添加google支付相关代码

This commit is contained in:
CounterFire2023 2023-07-09 19:19:12 +08:00
parent a40069ef27
commit e2ab4da847
7 changed files with 313 additions and 1 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ dist
tmp
target
boundle.log
google_cloud.json

View File

@ -26,6 +26,7 @@
"fastify": "^4.8.1",
"fastify-plugin": "^4.2.1",
"google-auth-library": "^8.5.2",
"googleapis": "^120.0.0",
"mongoose": "^6.6.5",
"mongoose-findorcreate": "^3.0.0",
"nanoid": "^3.1.23",
@ -35,8 +36,8 @@
},
"devDependencies": {
"@typegoose/typegoose": "^9.12.1",
"@types/node-schedule": "^2.1.0",
"@types/dotenv": "^8.2.0",
"@types/node-schedule": "^2.1.0",
"@typescript-eslint/eslint-plugin": "^5.40.1",
"@typescript-eslint/parser": "^5.40.1",
"eslint": "^8.25.0",

View File

@ -8,6 +8,7 @@ import config from 'config/config'
import { ConnectOptions } from 'mongoose'
import CodeTaskSchedule from 'schedule/codetask.schedule'
import { PriceSvr } from 'service/price.svr'
import { GooglePaySvr } from 'service/googlepay.svr'
const zReqParserPlugin = require('plugins/zReqParser')
@ -108,6 +109,10 @@ export class ApiServer {
logger.log(`DB Connection Error: ${err.message}`)
}
}
async initOtherServices() {
await new GooglePaySvr().init()
}
private setErrHandler() {
this.server.setNotFoundHandler(function (
request: any,
@ -167,6 +172,7 @@ export class ApiServer {
self.setErrHandler()
self.setFormatSend()
self.initSchedules()
await self.initOtherServices()
this.server.listen({ port: config.api.port, host: config.api.host }, (err: any, address: any) => {
if (err) {
logger.log(err)

View File

@ -0,0 +1,63 @@
import BaseController from 'common/base.controller'
import { ZError } from 'common/ZError'
import { router } from 'decorators/router'
import logger from 'logger/logger'
import { GoogleInApp, GooglePayStatus } from 'modules/GoogleInApp'
import { GooglePaySvr } from 'service/googlepay.svr'
class GooglePayController extends BaseController {
@router('post /pay/google/verify')
async verifyGooglePay(req, res) {
const user = req.user
const { list } = req.params
if (!list || !list.length) {
throw new ZError(10, 'purchase data is empty')
}
logger.info(`verify google purchase::list=${list}`)
let results = []
for (let sub of list) {
let infoRes = await new GooglePaySvr().fetchPurchaseData(sub.id, sub.token)
// logger.log(JSON.stringify(infoRes))
if (infoRes.status !== 200) {
throw new ZError(20, 'fetch purchase data failed')
}
let info = infoRes.data
let status = GooglePayStatus.PENDING
if (info.purchaseState === 0 && info.consumptionState === 0) {
status = GooglePayStatus.PURCHASED
}
if (status === GooglePayStatus.PURCHASED) {
try {
let record = await GoogleInApp.insertOrUpdate(
{ account: user.id, outOrderId: info.orderId },
{
purchaseToken: sub.token,
productId: sub.id,
gameOrderId: info.obfuscatedExternalAccountId,
country: info.regionCode,
status,
kind: info.kind,
purchaseTime: info.purchaseTimeMillis,
data: info,
$inc: { version: 1 },
},
)
await new GooglePaySvr().consumePurchase(sub.id, sub.token)
record.status = GooglePayStatus.SUCCESS
record.consumeTime = Date.now()
await record.save()
results.push({
productId: sub.id,
gameOrderId: record.gameOrderId,
orderId: record.outOrderId,
status: record.status,
})
} catch (err) {
logger.info(`consume purchase:: productId: ${sub.id} purchaseToken: ${sub.token} failed`, err.message || err)
}
}
}
//TODO:: 通知游戏服
return results
}
}

View File

@ -0,0 +1,85 @@
import { getModelForClass, index, modelOptions, mongoose, prop, ReturnModelType, Severity } from '@typegoose/typegoose'
import { dbconn } from 'decorators/dbconn'
import { BaseModule } from './Base'
export enum GooglePayStatus {
PENDING = 0, // 默认状态, 未支付
TRANSFERING = 1, //只有购买链上资产才会有该状态
TRANSFERED = 2, //只有购买链上资产才会有该状态
PURCHASED = 3, //用户已支付, 但未确认和消费
SUCCESS = 9,
VOIDED = 96, // 作废, 取消, 退款
USER_CANCEL = 97, // 用户取消
TRANSFER_FAIL = 98, // 转账错误
FAIL = 99, // 交易失败
}
@dbconn('pay')
@index({ gameOrderId: 1 }, { unique: true, partialFilterExpression: { gameOrderId: { $exists: true } } })
@index({ account: 1, outOrderId: 1 }, { unique: true })
@index({ account: 1 }, { unique: false })
@modelOptions({
schemaOptions: { collection: 'google_inapp_record', timestamps: true },
options: { allowMixed: Severity.ALLOW },
})
export class GoogleInAppClass extends BaseModule {
// 用户账号
@prop({ required: true })
public account: string
// 渠道返回的订单id
@prop()
public outOrderId: string
// 国家, 这里取自订单信息中返回的国家代码
@prop()
public country?: string
// 游戏服传入的订单id,
// 由于有非游戏内购买的可能, 所以该字段可能不存在
@prop()
public gameOrderId?: string
// 交易状态
@prop({ required: true, default: GooglePayStatus.PENDING })
public status: GooglePayStatus
// 渠道返回的原始资料
@prop({ type: mongoose.Schema.Types.Mixed })
public outData: any
// 对应google play console中的 productid
@prop({ required: true })
public productId: string
@prop()
public purchaseToken: string
@prop({ required: true, default: 1 })
public amount: number
// 表示 androidpublisherservice 中的 inappPurchase 对象
@prop()
public kind: string
// 支付时间, 取自渠道订单信息中的purchaseTimeMillis
@prop()
public purchaseTime: number
@prop()
public gameAccountId?: string
// 消费时间
@prop()
public consumeTime?: number
// voided 相关信息
@prop()
public voidedTime?: number
// 发起作废购买交易的发起者可能的值0。用户 1. 开发者 2. Google
@prop()
public voidedSource?: number
// 购买作废的原因可能为0。其他 1. 反悔 2。未收到 3. 有缺陷 4. Accidental_purchase 5. 欺诈 6. Friendly_fraud 7. 退款
@prop()
public voidedReason?: number
@prop({ default: 0 })
public version: number
public static async findByRecordId(this: ReturnModelType<typeof GoogleInAppClass>, outOrderId: string) {
return this.findOne({ outOrderId }).exec()
}
}
export const GoogleInApp = getModelForClass(GoogleInAppClass, { existingConnection: GoogleInAppClass.db })

View File

@ -0,0 +1,54 @@
import { singleton } from 'decorators/singleton'
import { androidpublisher_v3, google } from 'googleapis'
import logger from 'logger/logger'
const PAGEAGE_NAME = 'com.cege.games.release'
@singleton
export class GooglePaySvr {
private androidpublisher: androidpublisher_v3.Androidpublisher
constructor() {}
public async init() {
// this.androidpublisher = google.androidpublisher('v3')
const auth = new google.auth.GoogleAuth({
keyFile: 'google_cloud.json',
// Scopes can be specified either as an array or as a single, space-delimited string.
scopes: ['https://www.googleapis.com/auth/androidpublisher'],
})
this.androidpublisher = google.androidpublisher({
version: 'v3',
auth,
})
}
public async fetchPurchaseData(productId: string, purchaseToken: string) {
logger.info(`fetchPurchaseData::productId=${productId}, purchaseToken=${purchaseToken}`)
return this.androidpublisher.purchases.products.get({
packageName: PAGEAGE_NAME,
productId: productId,
token: purchaseToken,
})
}
public async consumePurchase(productId: string, purchaseToken: string) {
return this.androidpublisher.purchases.products.consume({
packageName: PAGEAGE_NAME,
productId: productId,
token: purchaseToken,
})
}
/**
*
* @param startTime
* @param endTime
*/
public async queryVoidedPurchases(startTime: string, endTime: string) {
return this.androidpublisher.purchases.voidedpurchases.list({
packageName: PAGEAGE_NAME,
type: 0,
startTime,
endTime,
})
}
}

102
yarn.lock
View File

@ -619,6 +619,14 @@ buffer@^6.0.3:
base64-js "^1.3.1"
ieee754 "^1.2.1"
call-bind@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
dependencies:
function-bind "^1.1.1"
get-intrinsic "^1.0.2"
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
@ -1168,6 +1176,24 @@ gcp-metadata@^5.0.0:
gaxios "^5.0.0"
json-bigint "^1.0.0"
gcp-metadata@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-5.3.0.tgz#6f45eb473d0cb47d15001476b48b663744d25408"
integrity sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==
dependencies:
gaxios "^5.0.0"
json-bigint "^1.0.0"
get-intrinsic@^1.0.2:
version "1.2.1"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82"
integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==
dependencies:
function-bind "^1.1.1"
has "^1.0.3"
has-proto "^1.0.1"
has-symbols "^1.0.3"
glob-parent@^5.1.2, glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
@ -1213,6 +1239,21 @@ globby@^11.1.0:
merge2 "^1.4.1"
slash "^3.0.0"
google-auth-library@^8.0.2:
version "8.9.0"
resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-8.9.0.tgz#15a271eb2ec35d43b81deb72211bd61b1ef14dd0"
integrity sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==
dependencies:
arrify "^2.0.0"
base64-js "^1.3.0"
ecdsa-sig-formatter "^1.0.11"
fast-text-encoding "^1.0.0"
gaxios "^5.0.0"
gcp-metadata "^5.3.0"
gtoken "^6.1.0"
jws "^4.0.0"
lru-cache "^6.0.0"
google-auth-library@^8.5.2:
version "8.5.2"
resolved "https://registry.npmmirror.com/google-auth-library/-/google-auth-library-8.5.2.tgz#bcdced8f7b475b80bf0e9c109c7c7e930866947b"
@ -1235,6 +1276,26 @@ google-p12-pem@^4.0.0:
dependencies:
node-forge "^1.3.1"
googleapis-common@^6.0.0:
version "6.0.4"
resolved "https://registry.yarnpkg.com/googleapis-common/-/googleapis-common-6.0.4.tgz#bd968bef2a478bcd3db51b27655502a11eaf8bf4"
integrity sha512-m4ErxGE8unR1z0VajT6AYk3s6a9gIMM6EkDZfkPnES8joeOlEtFEJeF8IyZkb0tjPXkktUfYrE4b3Li1DNyOwA==
dependencies:
extend "^3.0.2"
gaxios "^5.0.1"
google-auth-library "^8.0.2"
qs "^6.7.0"
url-template "^2.0.8"
uuid "^9.0.0"
googleapis@^120.0.0:
version "120.0.0"
resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-120.0.0.tgz#0ca260fad6cc7575e5d762e6a33ea88000f8e6bc"
integrity sha512-Reo5PpERv0Df/L8Jx8CrPHMI3oEXmPBDHLXCY12biXUtveVgWVEoQN4Inn/85+eoNRsDiVyeEH/MjAa3hPHHrA==
dependencies:
google-auth-library "^8.0.2"
googleapis-common "^6.0.0"
grapheme-splitter@^1.0.4:
version "1.0.4"
resolved "https://registry.npmmirror.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
@ -1254,6 +1315,16 @@ has-flag@^4.0.0:
resolved "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
has-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0"
integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==
has-symbols@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
has@^1.0.3:
version "1.0.3"
resolved "https://registry.npmmirror.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
@ -1780,6 +1851,11 @@ normalize-path@^3.0.0, normalize-path@~3.0.0:
resolved "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
object-inspect@^1.9.0:
version "1.12.3"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9"
integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==
obliterator@^2.0.1:
version "2.0.4"
resolved "https://registry.npmmirror.com/obliterator/-/obliterator-2.0.4.tgz#fa650e019b2d075d745e44f1effeb13a2adbe816"
@ -1940,6 +2016,13 @@ punycode@^2.1.0, punycode@^2.1.1:
resolved "https://registry.npmmirror.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
qs@^6.7.0:
version "6.11.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9"
integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==
dependencies:
side-channel "^1.0.4"
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
@ -2100,6 +2183,15 @@ shebang-regex@^3.0.0:
resolved "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
side-channel@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
dependencies:
call-bind "^1.0.0"
get-intrinsic "^1.0.2"
object-inspect "^1.9.0"
sift@16.0.0:
version "16.0.0"
resolved "https://registry.npmmirror.com/sift/-/sift-16.0.0.tgz#447991577db61f1a8fab727a8a98a6db57a23eb8"
@ -2356,6 +2448,16 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
url-template@^2.0.8:
version "2.0.8"
resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21"
integrity sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==
uuid@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
v8-compile-cache-lib@^3.0.1:
version "3.0.1"
resolved "https://registry.npmmirror.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"