Skip to content

Instantly share code, notes, and snippets.

@ezioruan
Last active June 10, 2023 01:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ezioruan/2073161332bc87846e27403f4a995346 to your computer and use it in GitHub Desktop.
Save ezioruan/2073161332bc87846e27403f4a995346 to your computer and use it in GitHub Desktop.
1: model 数据库设计
```
module.exports = function(sequelize, DataTypes) {
return sequelize.define('payment', {
id: {
type: DataTypes.INTEGER(10).UNSIGNED,
allowNull: false,
primaryKey: true,
autoIncrement: true
},
userId: {
type: DataTypes.INTEGER(10).UNSIGNED,
allowNull: false
},
productId: {
type: DataTypes.STRING(255),
allowNull: false
},
transactionId: {
type: DataTypes.STRING(255),
allowNull: false
},
originalTransactionId: {
type: DataTypes.STRING(255),
allowNull: true
},
receipt: {
type: DataTypes.TEXT,
allowNull: false
},
receiptData: {
type: DataTypes.JSON,
allowNull: true
},
expireDate: {
type: DataTypes.DATE,
allowNull: false
},
cancelDate: {
type: DataTypes.DATE,
allowNull: true
},
cancelReason: {
type: DataTypes.STRING(64),
allowNull: true
},
paymentType: {
type: DataTypes.STRING(64),
allowNull: true
},
createdAt: {
type: DataTypes.DATE,
allowNull: true
},
updatedAt: {
type: DataTypes.DATE,
allowNull: true
}
}, {
tableName: 'payment',
timestamps: true,
underscored: false
});
};
```
2: pay service, 处理订阅和插入表数据
```
import logger from '../logger'
import { panic } from '../error'
import * as db from '../db'
import _ from 'lodash'
import config from 'config'
import moment from 'moment'
import uuid from 'node-uuid'
import util from 'util'
import iap from 'iap'
import userServices from './user'
const verifyPayment = util.promisify(iap.verifyPayment)
const Apple = "apple"
const Google = "google"
class PayServices {
constructor() {}
async getProducts(platform) {
return await db.models.Product.findAll({
where: {
platform,
},
order: [
['price', 'desc']
]
})
}
async getProduct(platform, productId) {
return await db.models.Product.findOne({
where: {
productId,
platform,
}
})
}
async getPaymentByTranscationId(transactionId) {
return await db.models.Payment.findOne({ where: { transactionId } })
}
async getActivePayment(userId) {
const sql = `
select * from payment where
userId = ? and
expireDate > now()
order by createdAt desc limit 1
`
return await db.selectOne(sql, [userId])
}
async applePurchase(userId, productId, receipt) {
const product = await this.getProduct(Apple, productId)
if (!product) {
panic(50000, 'product not found')
}
let result
try {
result = await verifyPayment(Apple, {
receipt,
secret: config.iap.applePassword,
// excludeOldTransactions: true,
})
logger.debug("result", result)
} catch (err) {
logger.error("err", err)
panic(50001, `apple purchase validate error: ${err.message}`)
}
// get current subscription from in app
// find the most recently purchased product
const { in_app } = result.receipt
if (result.productId !== productId) {
panic(50002, 'productId not match')
}
const paymentsByProduct = _.filter(in_app, purchase => purchase.product_id === productId)
const subscription = _.first(_.sortBy(paymentsByProduct, purchase => -purchase.purchase_date_ms))
if (!subscription) {
panic(50003, 'subscription not found!')
}
logger.debug("subscription ", subscription)
let expireDate = moment(Number(subscription.expires_date_ms))
if (expireDate.isBefore(moment())) {
panic(50004, 'subscription has expired')
}
if (await this.getPaymentByTranscationId(result.transactionId)) {
panic(50005, 'subscription has been processed')
}
const activePayment = await this.getActivePayment(userId)
try {
if (activePayment) {
logger.error("get activityPayment", activePayment)
const activitePaymentInApp = _.first(_.filter(in_app, purchase => purchase.transaction_id === activePayment.transactionId))
logger.error("activitePaymentInApp", activitePaymentInApp)
const activePaymentExpiredDate = moment(Number(activitePaymentInApp.expires_date_ms))
if (activitePaymentInApp) {
if (activitePaymentInApp.is_trial_period) {
logger.debug("update activePayment to expired in trial period")
await db.models.Payment.update({
expireDate: moment(),
}, {
where: {
id: activePayment.id
}
})
} else if (activePaymentExpiredDate.isBefore(moment())) {
logger.debug("update activePayment to expired")
await db.models.Payment.update({
expireDate: activePaymentExpiredDate,
}, {
where: {
id: activePayment.id
}
})
} else {
panic(50006, 'you already have an active subscription')
}
}
}
} catch (err) {
logger.error("err on processe activePayment", err)
}
if (result.environment === 'sandbox') {
expireDate.add(config.iap.extraExpireUnit, 'minutes')
} else {
expireDate.add(config.iap.extraExpireUnit, 'days')
}
const payment = await db.models.Payment.create({
userId,
productId,
transactionId: result.transactionId,
originalTransactionId: subscription.original_transaction_id || result.transactionId,
expireDate,
receipt: receipt,
receiptData: result,
paymentType: 'purchase',
})
return payment
}
async googlePurchase(userId, productId, purchaseToken, orderId) {
const product = await this.getProduct(Google, productId)
if (!product) {
panic(50000, 'product not found')
}
let result
if (purchaseToken === "purchaseTokenTest") {
// test
result = {
receipt: {
kind: "androidpublisher#productPurchase",
purchaseTimeMillis: "1410835105408",
purchaseState: 0,
consumptionState: 1,
developerPayload: orderId,
packageName: config.iap.googlePackageName,
expiryTimeMillis: '1527152141520'
},
transactionId: "ghbbkjheodjokkipdmlkjajn.AO-J1OwfrtpJd2fkzzZqv7i107yPmaUD9Vauf9g5evoqbIVzdOGYyJTSEMhSTGFkCOzGtWccxe17dtbS1c16M2OryJZPJ3z-eYhEJYiSLHxEZLnUJ8yfBmI",
productId: "vibe.monthly",
platform: "google",
expireDate: "2018-06-16 09:01:01"
}
} else {
try {
logger.debug("config.iap.googleKeyObject:", config.iap.googleKeyObject)
result = await verifyPayment(Google, {
receipt: purchaseToken,
productId,
packageName: config.iap.googlePackageName,
subscription: true,
keyObject: config.iap.googleKeyObject,
})
logger.debug("result", result)
} catch (err) {
logger.error("err", err)
panic(50001, `google purchase validate error: ${err.message}`)
}
}
if (result.productId !== productId) {
panic(50002, 'productId not match')
}
// orderId from front end
let transactionId = result.receipt.orderId || orderId
let originalTransactionId
if (await this.getPaymentByTranscationId(transactionId)) {
panic(50005, 'subscription has been processed')
}
logger.info("userId!!!", userId)
const activePayment = await this.getActivePayment(userId)
logger.info("activePayment", activePayment)
if (activePayment && activePayment.originalTransactionId) {
originalTransactionId = activePayment.originalTransactionId
} else {
originalTransactionId = transactionId
}
logger.info("transactionId", transactionId)
logger.info("originalTransactionId", originalTransactionId)
let expireDate = moment(Number(result.receipt.expiryTimeMillis))
logger.debug("expireDate:", expireDate)
const payment = await db.models.Payment.create({
userId,
productId,
transactionId: transactionId,
originalTransactionId: originalTransactionId,
expireDate: expireDate,
receipt: purchaseToken,
receiptData: result,
paymentType: 'purchase',
})
logger.info("payment", payment)
try {
// Save the default schedule (every 3 hours) scheduleId = 9
logger.debug("saveReminderSchedule: userId", userId)
await userServices.saveReminderSchedule(userId, 9)
} catch (err) {
logger.error("saveReminderSchedule error:", err)
}
return payment
}
async getUserVibeFreeSubscriotion(userId) {
const transactionId = uuid.v4()
const [subscription, ] = await db.models.Payment.findOrCreate({
where: {
userId,
productId: 'vibe',
},
defaults: {
userId,
productId: 'vibe',
transactionId,
originalTransactionId: transactionId,
expireDate: moment(),
receipt: '',
receiptData: {},
paymentType: 'purchase',
}
})
return subscription
}
async processVibeFreeSubscription(email, expireDate) {
const user = await userServices.getUserByEmail(email)
expireDate = moment(expireDate)
if (!expireDate.isValid()) {
panic(50006, 'expireDate is not a valid date')
}
const vibeFreeSubscription = await this.getUserVibeFreeSubscriotion(user.id)
await vibeFreeSubscription.update({
expireDate,
})
return vibeFreeSubscription
}
}
const payServices = new PayServices()
export default payServices
```
3: 定时任务逻辑, 处理丢单和refund
```
const config = require('config')
// const { Op } = require('sequelize')
const { colorConsole } = require('tracer')
const moment = require('moment');
const iap = require('iap')
const util = require('util')
const _ = require('lodash')
const models = require('./models')
const verifyPayment = util.promisify(iap.verifyPayment)
const logger = colorConsole('debug')
const getProducts = async() => {
return await models.Product.findAll()
}
const checkRefund = async() => {
const payments = await models.Payment.findAll({
where: {
cancelDate: null,
}
})
for (const payment of payments) {
try {
const result = await verifyPayment('apple', {
receipt: payment.receipt,
productId: payment.productId,
secret: config.iap.applePassword,
})
const { latestReceiptInfo } = result
const latestPurchase = _.last(latestReceiptInfo);
const cancellation_date_ms = _.get(latestPurchase, 'cancellation_date_ms', null);
if (!cancellation_date_ms) {
continue
}
const cancelDate = moment(Number(cancellation_date_ms)).format();
await payment.update({
cancelDate,
cancelReason: latestPurchase.cancellation_reason
})
} catch (err) {
logger.error("err", err)
}
}
}
const hasTransactionIdExists = async transactionId => {
const payment = await models.Payment.findOne({
where: { transactionId }
})
return payment !== null
}
const checkRenew = async() => {
const payments = await models.Payment.findAll({
where: {
cancelDate: null,
// transactionId: 1000000388988732,
// expireDate: {
// [Op.between]: [moment().subtract('3', 'days').format(), moment().format()]
// },
paymentType: 'purchase'
}
})
const products = await getProducts();
for (const payment of payments) {
let platform
for (let product of products) {
if (product.productId === payment.productId) {
platform = product.platform
break
}
}
logger.debug('platform', platform)
if (platform === 'apple') {
try {
const result = await verifyPayment('apple', {
receipt: payment.receipt,
secret: config.iap.applePassword,
excludeOldTransactions: true,
})
const { latestReceiptInfo } = result
const latestPurchase = _.last(latestReceiptInfo);
let expireDate = _.get(latestPurchase, 'expires_date_ms', null);
if (!expireDate) {
continue
}
expireDate = moment(Number(expireDate))
const transactionIdExists = await hasTransactionIdExists(latestPurchase.transaction_id)
if (!transactionIdExists) {
if (result.environment === 'sandbox') {
expireDate.add(config.iap.extraExpireUnit, 'minutes')
} else {
expireDate.add(config.iap.extraExpireUnit, 'days')
}
const newPayment = await models.Payment.create({
userId: payment.userId,
productId: latestPurchase.product_id,
transactionId: latestPurchase.transaction_id,
originalTransactionId: latestPurchase.original_transaction_id,
expireDate,
receipt: payment.receipt,
receiptData: result,
paymentType: 'renew',
})
logger.debug("renew", newPayment.id)
}
} catch (err) {
logger.error("err", err)
}
} else if (platform === 'google') {
try {
const result = await verifyPayment('google', {
receipt: payment.receipt,
productId: payment.productId,
packageName: config.iap.googlePackageName,
subscription: true,
keyObject: config.iap.googleKeyObject,
})
// renew
if (result.receipt.autoRenewing) {
const expireDate = moment(Number(result.receipt.expiryTimeMillis))
if (expireDate.isAfter(payment.expireDate)) {
if (config.util.getEnv('NODE_ENV') === 'production') {
expireDate.add(config.iap.extraExpireUnit, 'days')
} else {
expireDate.add(config.iap.extraExpireUnit, 'minutes')
}
let transactionId = result.receipt.orderId
if (transactionId === payment.originalTransactionId) {
// new transactionId
transactionId = `${transactionId}-${result.receipt.expiryTimeMillis}`
}
logger.info("transactionId", transactionId)
logger.info("result.receipt.orderId", result.receipt.orderId)
let now = moment()
logger.info("update Payment expireDate")
await models.Payment.update({
expireDate,
updatedAt: now
}, {
where: {
id: payment.id
}
})
const newPayment = await models.Payment.create({
userId: payment.userId,
productId: payment.productId,
transactionId: transactionId,
originalTransactionId: payment.originalTransactionId,
expireDate,
receipt: payment.receipt,
receiptData: result,
paymentType: 'renew',
})
logger.info("newPayment", newPayment)
}
}
// refund
if (result.receipt.userCancellationTimeMillis) {
const cancelDate = moment(Number(result.receipt.userCancellationTimeMillis))
logger.info("update Payment expireDate payment.originalTransactionId", payment.originalTransactionId)
let now = moment()
await models.Payment.update({
expireDate: cancelDate,
cancelDate,
cancelReason: result.receipt.cancelReason,
updatedAt: now
}, {
where: {
originalTransactionId: payment.originalTransactionId
}
})
}
} catch (err) {
logger.error("err", err)
}
}
}
}
const run = async() => {
// await checkRefund()
await checkRenew()
logger.info("process done")
process.exit()
}
run()
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment