Como integrar o Cash Shop (WCoin) ao site do servidor de MU
Conecte o Cash Shop do jogo ao site do seu servidor de MU Online gerenciando saldo de WCoin, WCoinP e Goblin Point com transações atômicas em SQL, painel de recarga e entrega segura de créditos.
O Cash Shop é onde o servidor de MU Online transforma engajamento em receita, e o site é a porta de entrada desse fluxo. Quando um jogador recarrega créditos no painel web, ele espera abrir a loja dentro do jogo e ver o saldo lá. Fazer essa ponte parecer instantânea e, principalmente, à prova de fal
O Cash Shop é onde o servidor de MU Online transforma engajamento em receita, e o site é a porta de entrada desse fluxo. Quando um jogador recarrega créditos no painel web, ele espera abrir a loja dentro do jogo e ver o saldo lá. Fazer essa ponte parecer instantânea e, principalmente, à prova de falhas é o que separa um sistema de cash confiável de um que gera reclamação e prejuízo. Neste tutorial você vai construir a integração entre o site e o Cash Shop cuidando do que realmente importa: onde o saldo mora no banco, como creditá-lo com segurança transacional e como evitar créditos duplicados ou perdidos. Os nomes de colunas e tabelas variam por versão do emulador, então trate cada trecho de código como exemplo a ser adaptado ao seu schema.
Antes de tocar em qualquer coisa, é importante entender que o Cash Shop do cliente MU não fala com o site diretamente. Ele lê um saldo numérico que está na tabela de contas do banco. O site, portanto, não "conversa" com a loja: ele apenas altera esse número. Toda a integração se resume a manipular esse saldo de forma correta e a registrar cada movimento. Essa clareza mental evita que você procure APIs que não existem e foque na parte que de fato importa, que é o banco de dados.
Pré-requisitos
Se o servidor ainda não está no ar, siga primeiro o guia de como criar servidor de MU Online. Com o servidor pronto, você precisa de:
- SQL Server com o banco
MuOnlinee acesso à tabela de contas (MEMB_INFOou equivalente da sua versão). - PHP 7.4 ou superior com a extensão
sqlsrv/pdo_sqlsrvhabilitada. - Sistema de login do site já funcionando, com sessão do jogador autenticada.
- Usuário SQL específico do site com permissão de leitura e escrita apenas nas colunas de saldo e nas tabelas de log, nunca com
db_owner. - Conhecimento das colunas de moeda da sua versão: WCoinC, WCoinP, GoblinPoint ou os nomes equivalentes.
- Um gateway de pagamento já escolhido (o crédito só deve ser liberado após confirmação de pagamento).
Confirme antes de tudo quais colunas de moeda existem no seu banco. Uma consulta rápida evita dor de cabeça:
SELECT COLUMN_NAME, DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'MEMB_INFO'
AND COLUMN_NAME IN ('WCoinC', 'WCoinP', 'GoblinPoint', 'GameIDC');
Se as colunas não aparecerem com esses nomes, procure na documentação do seu emulador. Alterar a coluna errada faz o saldo somar em um lugar que a loja do jogo não lê.
Onde o saldo de moeda mora
As três moedas mais comuns do Cash Shop ficam, na maioria das versões, na própria tabela de contas. Entender o papel de cada uma orienta o que o site deve creditar.
| Moeda | Coluna típica | Uso comum | Origem no site |
|---|---|---|---|
| WCoin / WCoinC | WCoinC | Moeda principal comprável | Recarga paga |
| WCoinP | WCoinP | Moeda premium ou de bônus | Bônus, promoções |
| Goblin Point | GoblinPoint | Moeda de eventos e lojas alternativas | Recompensas, eventos |
O Cash Shop dentro do jogo lê esses campos quando o jogador abre a loja. Isso significa que o trabalho do site é, essencialmente, executar um UPDATE seguro nessas colunas e registrar a operação. Toda a complexidade está em fazer esse UPDATE de forma que nunca credite a mais nem a menos, mesmo diante de falhas de rede, cliques duplos e webhooks repetidos.
Modelagem das tabelas de controle
Nunca credite direto sem registrar. Crie duas tabelas de apoio: uma para o histórico de transações e outra que serve de trava de idempotência. Em muitos casos uma única tabela bem modelada cumpre os dois papéis.
CREATE TABLE CashShop_Transacao (
Id BIGINT IDENTITY(1,1) PRIMARY KEY,
ContaId VARCHAR(10) NOT NULL,
Moeda VARCHAR(20) NOT NULL, -- 'WCoinC', 'WCoinP', 'GoblinPoint'
Quantidade INT NOT NULL,
SaldoAntes INT NOT NULL,
SaldoDepois INT NOT NULL,
Origem VARCHAR(40) NOT NULL, -- 'mercadopago', 'admin', 'bonus'
GatewayRef VARCHAR(80) NULL, -- ID do pagamento (idempotência)
CriadoEm DATETIME NOT NULL DEFAULT GETDATE()
);
GO
-- Índice único garante que o mesmo pagamento nunca credite duas vezes
CREATE UNIQUE INDEX UX_CashShop_GatewayRef
ON CashShop_Transacao (GatewayRef)
WHERE GatewayRef IS NOT NULL;
GO
O índice único filtrado é a peça central da segurança. Ele torna fisicamente impossível gravar duas transações com o mesmo GatewayRef. Se um webhook chegar duplicado, a segunda inserção falha no banco, e o seu código trata esse erro como "já processado". Você não precisa confiar apenas na lógica da aplicação; o banco impõe a regra.
Guardar SaldoAntes e SaldoDepois parece redundante, mas é ouro para auditoria. Quando um jogador abre disputa dizendo que não recebeu, você tem o valor exato antes e depois de cada operação, com carimbo de tempo. Isso resolve reclamações em segundos.
O procedimento de crédito com transação
O coração do sistema é a operação de creditar. Ela precisa ser atômica: ler o saldo, somar, gravar o novo saldo e registrar a transação, tudo dentro de uma única transação SQL. Uma stored procedure encapsula essa lógica e a torna reutilizável.
CREATE PROCEDURE dbo.CreditarWCoin
@ContaId VARCHAR(10),
@Quantidade INT,
@Origem VARCHAR(40),
@GatewayRef VARCHAR(80) = NULL
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON; -- qualquer erro reverte a transação
BEGIN TRAN;
-- Trava a linha da conta para leitura consistente
DECLARE @saldoAntes INT;
SELECT @saldoAntes = WCoinC
FROM MEMB_INFO WITH (UPDLOCK, ROWLOCK)
WHERE memb___id = @ContaId;
IF @saldoAntes IS NULL
BEGIN
ROLLBACK TRAN;
THROW 50001, 'Conta inexistente', 1;
END
-- Idempotência: se já processado, aborta silenciosamente
IF @GatewayRef IS NOT NULL AND EXISTS (
SELECT 1 FROM CashShop_Transacao WHERE GatewayRef = @GatewayRef)
BEGIN
ROLLBACK TRAN;
RETURN; -- já creditado antes
END
UPDATE MEMB_INFO
SET WCoinC = WCoinC + @Quantidade
WHERE memb___id = @ContaId;
INSERT INTO CashShop_Transacao
(ContaId, Moeda, Quantidade, SaldoAntes, SaldoDepois, Origem, GatewayRef)
VALUES
(@ContaId, 'WCoinC', @Quantidade, @saldoAntes,
@saldoAntes + @Quantidade, @Origem, @GatewayRef);
COMMIT TRAN;
END
GO
Três detalhes fazem essa procedure ser segura. O XACT_ABORT ON garante que qualquer erro reverte tudo automaticamente. O WITH (UPDLOCK, ROWLOCK) na leitura do saldo trava a linha da conta para que duas execuções simultâneas não leiam o mesmo saldo e sobrescrevam uma à outra, o que causaria perda de crédito. E a checagem de GatewayRef dá a segunda camada de idempotência, complementando o índice único. Com isso, mesmo dois webhooks chegando no mesmo milissegundo resultam em um único crédito.
Camada PHP que chama o crédito
No PHP, a chamada fica limpa porque toda a complexidade está na procedure. O papel do PHP é validar quem está pedindo e passar os parâmetros com binding.
<?php
// lib/cashshop.php
require_once __DIR__ . '/../config/db.php';
function creditarWCoin(string $contaId, int $qtd, string $origem, ?string $ref = null): bool {
if ($qtd <= 0) return false;
$conn = getDB();
$sql = "{CALL dbo.CreditarWCoin(?, ?, ?, ?)}";
$params = [
[$contaId, SQLSRV_PARAM_IN],
[$qtd, SQLSRV_PARAM_IN],
[$origem, SQLSRV_PARAM_IN],
[$ref, SQLSRV_PARAM_IN],
];
$stmt = sqlsrv_query($conn, $sql, $params);
if ($stmt === false) {
error_log('Erro ao creditar WCoin: ' . print_r(sqlsrv_errors(), true));
return false;
}
return true;
}
O uso de parâmetros vinculados (?) é inegociável. Nunca monte a chamada concatenando o $contaId ou a quantidade diretamente na string, porque isso abre porta para injeção de SQL. Com binding, o driver trata cada valor como dado, não como comando, e o ataque simplesmente não funciona.
Painel de recarga no site
O painel que o jogador vê organiza os pacotes de créditos e leva ao pagamento. O crédito em si só é liberado depois que o gateway confirma, nunca no clique do botão.
<?php
// recarga.php
session_start();
require_once __DIR__ . '/../lib/cashshop.php';
if (empty($_SESSION['conta_mu'])) {
header('Location: /login');
exit;
}
$pacotes = [
'p1' => ['wcoin' => 1000, 'preco' => 'R$ 10,00'],
'p2' => ['wcoin' => 2500, 'preco' => 'R$ 20,00', 'bonus' => 300],
'p3' => ['wcoin' => 6000, 'preco' => 'R$ 40,00', 'bonus' => 1000],
];
// Consultar saldo atual para exibir
$conn = getDB();
$stmt = sqlsrv_query($conn,
"SELECT WCoinC, WCoinP FROM MEMB_INFO WHERE memb___id = ?",
[[$_SESSION['conta_mu'], SQLSRV_PARAM_IN]]);
$saldo = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
?>
<h1>Recarga de WCoin</h1>
<p>Saldo atual: <strong><?= (int)$saldo['WCoinC'] ?> WCoin</strong></p>
<div class="pacotes">
<?php foreach ($pacotes as $id => $p): ?>
<div class="pacote">
<h3><?= number_format($p['wcoin'], 0, ',', '.') ?> WCoin</h3>
<?php if (!empty($p['bonus'])): ?>
<span class="bonus">+<?= $p['bonus'] ?> de bônus</span>
<?php endif; ?>
<p class="preco"><?= $p['preco'] ?></p>
<a href="/pagamento?pacote=<?= $id ?>" class="btn">Comprar</a>
</div>
<?php endforeach; ?>
</div>
O botão leva para o fluxo de pagamento, não para o crédito. Essa separação é uma regra de segurança fundamental: o crédito acontece exclusivamente no callback do gateway, quando você tem certeza de que o dinheiro entrou. Creditar no clique permitiria a qualquer um ganhar WCoin de graça só chamando a URL.
Entrega do crédito após confirmação do pagamento
Quando o gateway confirma o pagamento, ele chama seu endpoint de callback. É ali que o crédito acontece, usando o ID do pagamento como chave de idempotência.
<?php
// callback-pagamento.php (chamado pelo gateway, não pelo jogador)
require_once __DIR__ . '/../lib/cashshop.php';
// 1) Validar a autenticidade da notificação (assinatura/token do gateway)
// A forma varia por gateway; NUNCA credite sem validar.
// 2) Recuperar os dados confirmados
$pagamentoId = $notificacao['id']; // ID único do gateway
$statusPago = $notificacao['status'] === 'approved';
$contaId = $notificacao['metadata']['conta'];
$wcoin = (int)$notificacao['metadata']['wcoin'];
// 3) Creditar apenas se aprovado, usando o ID como referência
if ($statusPago) {
$ok = creditarWCoin($contaId, $wcoin, 'gateway', $pagamentoId);
// Se o webhook repetir, o índice único bloqueia o segundo crédito
http_response_code($ok ? 200 : 500);
} else {
http_response_code(200); // reconhece a notificação mesmo sem creditar
}
Note que o $pagamentoId vira o GatewayRef da transação. Se o gateway reenviar a notificação, coisa comum e esperada, a segunda tentativa bate no índice único e não credita de novo. Essa é a proteção que impede que uma retentativa de rede vire WCoin dobrado.
Quando o jogador vê o saldo no jogo
Um ponto que gera muitas dúvidas de suporte: creditei no banco, por que o jogador não vê? A resposta depende de como o Cash Shop lê o saldo, e isso varia por versão. Em geral há dois comportamentos:
- Leitura direta do banco a cada abertura da loja: o jogador só precisa fechar e reabrir o Cash Shop. É o caso mais comum e o mais confortável.
- Saldo em cache no GameServer: o servidor mantém o valor em memória e só recarrega ao logar. Aqui o jogador precisa relogar para ver o crédito.
Deixe isso claro na tela de sucesso da recarga, algo como "seu WCoin foi creditado; reabra a loja ou relogue para visualizar". Um aviso simples elimina metade dos tickets de suporte. Se o seu emulador oferece um comando ou porta de recarga em tempo real no GameServer, integrar por ali entrega a melhor experiência, mas não é obrigatório para um sistema funcional.
Segurança e boas práticas
O sistema de cash mexe com dinheiro real, então merece rigor extra. Além do binding de parâmetros e da idempotência já cobertos, aplique estes princípios:
- Menor privilégio no banco: o usuário SQL do site só precisa de
EXECUTEna procedure de crédito eSELECTnas colunas de saldo, nada além disso. - Validação da notificação do gateway: valide a assinatura ou token de toda notificação de pagamento; sem isso, qualquer um pode forjar um callback e ganhar créditos.
- Log completo e imutável: nunca permita
DELETEouUPDATEna tabela de transações pelo usuário do site; ela é um livro-razão. - Limites de sanidade: rejeite quantidades absurdas de WCoin por transação; um valor muito acima do maior pacote é sinal de manipulação.
- HTTPS obrigatório: todo o fluxo de recarga e callback tem que trafegar sob TLS.
Erros comuns e soluções
| Sintoma | Causa provável | Solução |
|---|---|---|
| Crédito não aparece no jogo | Cash Shop lê saldo em cache | Peça ao jogador para relogar; documente na tela |
| Jogador recebeu WCoin em dobro | Webhook duplicado sem idempotência | Adicione o índice único em GatewayRef e a checagem na procedure |
| Saldo somou na coluna errada | Coluna de moeda incorreta | Confirme o nome real da coluna no schema antes de creditar |
| Crédito liberado sem pagamento | Crédito no clique em vez do callback | Mova o crédito para o callback validado do gateway |
| Erro de deadlock sob concorrência | Ordem de travas inconsistente | Use UPDLOCK, ROWLOCK e mantenha a transação curta |
sqlsrv_query retorna false ao creditar | Permissão insuficiente | Conceda EXECUTE na procedure ao usuário do site |
| Alguém forjou uma notificação | Callback sem validação de assinatura | Valide token/assinatura de toda notificação antes de creditar |
Checklist de lançamento
- Colunas de moeda (
WCoinC,WCoinP,GoblinPoint) confirmadas no schema real - Tabela
CashShop_Transacaocriada comSaldoAnteseSaldoDepois - Índice único em
GatewayReffuncionando - Stored procedure
CreditarWCoincomXACT_ABORTeUPDLOCK - Usuário SQL do site com apenas
EXECUTEeSELECTnecessários - PHP usando parâmetros vinculados em toda chamada
- Crédito acontecendo somente no callback validado do gateway
- Validação de assinatura/token da notificação de pagamento implementada
- Tela de sucesso avisando sobre reabrir a loja ou relogar
- Limites de sanidade por transação configurados
- Todo o fluxo sob HTTPS
- Teste de webhook duplicado confirmando crédito único
- Teste de conta inexistente retornando erro tratado
Com essa base, o Cash Shop do seu site credita moeda de forma confiável, audita cada centavo e resiste às falhas de rede que inevitavelmente acontecem. A lição que atravessa tudo é a mesma: crédito é dinheiro, então trate cada operação como transação bancária, com atomicidade, idempotência e registro completo.
Perguntas frequentes
Qual a diferença entre WCoin, WCoinP e Goblin Point?
WCoin (também chamado WCoinC) é a moeda de créditos comprável, WCoinP é a moeda premium ou de bônus, e Goblin Point é uma moeda alternativa usada em alguns eventos e lojas. O nome e a coluna exata variam por versão, mas as três ficam na tabela de contas e são lidas pelo Cash Shop dentro do jogo.
O crédito adicionado no site aparece na hora no jogo?
Depende de onde o Cash Shop lê o saldo. Se ele lê direto da tabela de contas a cada abertura da loja, o jogador só precisa reabrir o Cash Shop. Alguns servidores mantêm o saldo em cache no GameServer, e nesse caso o jogador precisa relogar para atualizar.
Por que preciso de transação SQL para adicionar créditos?
Porque adicionar crédito envolve ler o saldo, somar e gravar, além de registrar o log da compra. Se o processo falhar no meio sem transação, o jogador pode receber crédito sem registro ou pagar sem receber. A transação garante que ou tudo acontece, ou nada acontece.
Como evito que alguém receba crédito duas vezes pelo mesmo pagamento?
Use uma chave de idempotência única por transação, normalmente o ID do pagamento do gateway, gravada em uma tabela de controle com índice único. Antes de creditar, verifique se aquele ID já foi processado; se já foi, ignore. Isso neutraliza webhooks duplicados e recargas de página.
Preciso de coluna nova na tabela de contas para o WCoin?
Na maioria das versões a coluna já existe, com nome como WCoinC, WCoinP ou GameIDC dependendo do emulador. Confirme o schema antes de criar qualquer coisa; criar coluna duplicada pode fazer o Cash Shop do jogo ler o saldo errado.