O maior portal de MU Online do Brasil — desde 2003
Tutorial Avançado Website

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.

GA Gabriel · Atualizado em 2 jul 2025 · ⏱ 26 min de leitura
Resposta rápida

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 MuOnline e acesso à tabela de contas (MEMB_INFO ou equivalente da sua versão).
  • PHP 7.4 ou superior com a extensão sqlsrv/pdo_sqlsrv habilitada.
  • 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.

MoedaColuna típicaUso comumOrigem no site
WCoin / WCoinCWCoinCMoeda principal comprávelRecarga paga
WCoinPWCoinPMoeda premium ou de bônusBônus, promoções
Goblin PointGoblinPointMoeda de eventos e lojas alternativasRecompensas, 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:

  1. 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.
  2. 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 EXECUTE na procedure de crédito e SELECT nas 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 DELETE ou UPDATE na 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

SintomaCausa provávelSolução
Crédito não aparece no jogoCash Shop lê saldo em cachePeça ao jogador para relogar; documente na tela
Jogador recebeu WCoin em dobroWebhook duplicado sem idempotênciaAdicione o índice único em GatewayRef e a checagem na procedure
Saldo somou na coluna erradaColuna de moeda incorretaConfirme o nome real da coluna no schema antes de creditar
Crédito liberado sem pagamentoCrédito no clique em vez do callbackMova o crédito para o callback validado do gateway
Erro de deadlock sob concorrênciaOrdem de travas inconsistenteUse UPDLOCK, ROWLOCK e mantenha a transação curta
sqlsrv_query retorna false ao creditarPermissão insuficienteConceda EXECUTE na procedure ao usuário do site
Alguém forjou uma notificaçãoCallback sem validação de assinaturaValide 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_Transacao criada com SaldoAntes e SaldoDepois
  • Índice único em GatewayRef funcionando
  • Stored procedure CreditarWCoin com XACT_ABORT e UPDLOCK
  • Usuário SQL do site com apenas EXECUTE e SELECT necessá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.

GA
Editor de guias e builds

Gabriel cobre gameplay, builds de classes, PvP e progressão. Testa cada estratégia em servidor antes de publicar.

Continue lendo

Artigos relacionados