遊戲禮包啟用碼案例分析

RexGene發表於2014-11-11

前言


        最近我們遊戲有一個通過啟用碼領取禮包的需求,需求大概是這樣:

  • 伺服器收到邀請碼後,能判斷啟用碼是否過期
  • 同一個啟用碼只能啟用一次
  • 一個玩家只能對一個禮包的啟用碼進行啟用

        其中,負責該案的策劃同學給了我一種方案,可是我覺得並不是十分合適,該方案大概是這樣,由開放商(也就是我們)去隨機生成一系列的啟用碼,每個啟用碼分別通過配置來繫結對應的禮包Id及有效期,然後再分發到運營渠道去進行活動推廣。

        當然這個方法不是說行不通,但是這個方案最大的問題就是維護複雜度過高,後期維護起來有一定的工作量,而且還要承擔維護過程中出現問題所造成的風險。試想一下,假如一個渠道需要開放1000個啟用碼,每個啟用碼還需要自己去手工配禮包的Id與有效期,只有一個禮包倒還好,如果出現多個禮包,多個有效期的話,在龐大的資料面前,出錯率明顯會上升,再加上後續會持續新增新的啟用碼,還要清理已經過期的啟用碼,新增新啟用碼的時候還得避免本次生成的啟用碼在之前沒有用過...光是這一系列的工作量,就已經需要單獨安排一個人去負責這個啟用碼的後續維護工作,而且維護過程中所存在的風險是無法避免的。

        如果要想避免上述中維護所帶來的問題,最好的做法就是避免後續維護,從而降低工作量與維護風險。上述維護的內容主要是把啟用碼跟禮包Id與有效期繫結的一個過程,如果讓啟用碼自身就帶有這些資訊的話,這個維護的過程就可以省下來了。也就是說,我們需要做的事情其實很簡單,只需要設計一個合理的生成啟用碼演算法就可以了。

可行性分析


        由於啟用碼是需要玩家輸入的,所以啟用碼有長度限制,不能太長。要是長度真的太長的話,我們可以考慮做其他的輸入方式,比如掃條形碼,不過這是後話。以目前的業務來看的話,我們啟用碼所包含的內容不需要太多,只需要一個對應的禮包Id與對應的有效期就可以了,當然這裡的有效期可以跟禮包Id通過配置表關聯起來,但是為了後期業務的靈活變更,目前還是把有效期放在了啟用碼裡面。如果只把禮包Id與有效期作為資料來源去生成啟用碼的話,每個啟用碼都會一樣的,所以我們還需要加入一個隨機數作為Key,來去對資料來源進行編碼,這樣我們就可以生成不同的啟用碼了。最後為了保證啟用碼的合法性,我們還需要生成一個check sum放到啟用碼裡面,這樣我們的啟用碼所需要的資料基本上就已經齊全了。

        接著就是對每項資料劃分大小,讓生成的啟用碼長度能保證在能接受的範圍之內。目前我們的啟用碼,所需要的資料分別是

  1. 禮包Id
  2. 啟始日期
  3. 失效日期
  4. 隨機祕鑰
  5. 校驗碼

        在這裡,有兩項資料的大小是有決定性作用的,禮包Id的大小會直接影響到後續的禮包數擴充套件數量限制,隨機祕鑰的大小範圍決定了同樣禮包Id與同樣有效時間所能生成的最大啟用碼數量。至於其他的資料項可以有更多的優化空間,有效期的單位有必要的話可以適當地擴大。為了後續的靈活控制,在這我還是選擇使用時間戳。校驗碼的大小可以適當按需求調整控制。目前劃分的資料所生成的啟用碼長度為32個字元,我認為還是能在接受範圍內的,所以這個思路可以執行下去。


案例程式碼

#include <string>
#include <random>
#include <memory>
#include <vector>
#include <iostream>
#include <sstream>

typedef std::shared_ptr<std::vector<std::string> > StringVectorPtr;

struct SFormatParam {
  uint16_t randomNumber;
  uint16_t giftId;
  uint32_t beginTime;
  uint32_t endTime;
  uint32_t checkSum;
};

char g_hexTable[] = "0123456789ABCDEF";
char g_keyBuffer[32];

void printUsage(const char* appName) {
  std::cout << "Usage:" << std::endl << std::endl;
  std::cout << "If you want to make gift code, you must like this:" << std::endl;
  std::cout << appName << " <gift-id> <begin-time-stamp> <end-time-stamp> <make-amount>" << std::endl << std::endl;
  std::cout << "And then, if you want to check gift code, you must like this:" << std::endl;
  std::cout << appName << " <gift-code>" << std::endl;
}

uint8_t hexToNumber(char hexChar) {
  if (hexChar >= '0' && hexChar <= '9') {
    return hexChar - '0';
  } else if (hexChar >= 'a' && hexChar <= 'f') {
    return hexChar - 'a' + 10;
  } else if (hexChar >= 'A' && hexChar <= 'F') {
    return hexChar - 'A' + 10;
  } else {
    printf("[Error]hexToNumber hexChar\n");
    return 0;
  }
}

bool hexToData(const char hex[], uint8_t data[], size_t len) {
  for (size_t i = 0; i < len; ++i) {
    size_t j = i << 1;
    int count = 2;

    for (int k = 0; k < count; ++k) {
      uint8_t byte = hex[j + k];
      if ((byte < '0' || byte > '9') && (byte < 'a' || byte > 'f') && (byte < 'A' || byte > 'F')) {
        return false;
      }
    }

    data[i] = static_cast<uint8_t>((hexToNumber(hex[j]) << 4) | hexToNumber(hex[j + 1]));
  }

  return true;
}

void dataToHex(const uint8_t data[], char outStr[], size_t len) {
  for (size_t i = 0; i < len; ++i) {
    uint8_t byte = data[i];
    size_t j = i << 1;

    outStr[j] = g_hexTable[byte >> 4];
    outStr[j + 1] = g_hexTable[byte & 0x0F];
  }
}

uint32_t getCheckSum(SFormatParam& outParam) {
  return ((outParam.randomNumber << 16 )+ outParam.giftId) ^ (outParam.beginTime + outParam.endTime);
}

bool getCodeInfo(const char hexStr[], SFormatParam& outParam) {
  if (!hexToData(hexStr, reinterpret_cast<uint8_t*>(&outParam), sizeof(outParam))) {
    return false;
  }

  uint32_t checkSum = getCheckSum(outParam);
  if (outParam.checkSum != checkSum) {
    printf("[Error]check sum error! check sum should be %x, but now is %x\n", checkSum, outParam.checkSum);
    return false;
  }

  uint16_t key = ~outParam.randomNumber;
  outParam.giftId ^= key;
  outParam.beginTime ^= key;
  outParam.beginTime ^= key << 16;
  outParam.endTime ^= key;
  outParam.endTime ^= key << 16;

  return true;
}

StringVectorPtr getGiftCode(uint32_t giftId, uint32_t beginTime, uint32_t endTime, uint32_t amount) {
  StringVectorPtr resultPtr(new std::vector<std::string>);
  srand(time(nullptr));

  while (amount--) {
    SFormatParam param = {
      .randomNumber = 0,
      .giftId = static_cast<uint16_t>(giftId),
      .beginTime = beginTime,
      .endTime = endTime,
    };

    param.randomNumber = static_cast<uint16_t>(rand() & ((1 << 16) - 1));

    uint16_t key = ~param.randomNumber;
    param.giftId ^= key;
    param.beginTime ^= key;
    param.beginTime ^= key << 16;
    param.endTime ^= key;
    param.endTime ^= key << 16;
    param.checkSum = getCheckSum(param),

    dataToHex(reinterpret_cast<const uint8_t*>(¶m), g_keyBuffer, sizeof(param));

    resultPtr->push_back(std::string(g_keyBuffer));
  }

  return resultPtr;
}

int main(int argc, char* args[]) {
  if (argc == 5) {
    StringVectorPtr ptr = getGiftCode(atoi(args[1]), atoi(args[2]), atoi(args[3]), atoi(args[4]));
    for (auto code : *ptr) {
      std::cout << code << std::endl;
    }
  } else if (argc == 2) {
    SFormatParam param;
    if (getCodeInfo(args[1], param)) {
      std::cout << "gift id:" << param.giftId << std::endl;
      std::cout << "begin time:" << param.beginTime << std::endl;
      std::cout << "end time:" << param.endTime << std::endl;
    }
  } else {
    printUsage(args[0]);
  }
}


相關文章