增加订阅消息的发送逻辑
This commit is contained in:
parent
6d906615cd
commit
221fffb169
@ -6,6 +6,6 @@ API_TOKEN_EXPIRESIN=1d
|
||||
DB_MAIN=mongodb://localhost/ghost-development
|
||||
DB_PIKACHU=mongodb://localhost/pikachu-development
|
||||
|
||||
REDIS=redis://10.10.4.8:6379/13
|
||||
REDIS=redis://localhost:6379
|
||||
|
||||
SUSU_HOST = http://154.8.214.202:8010
|
||||
|
@ -6,6 +6,6 @@ API_TOKEN_EXPIRESIN=1d
|
||||
DB_MAIN=mongodb://10.10.5.6/ghost-production
|
||||
DB_PIKACHU=mongodb://10.10.5.6/pikachu-production
|
||||
|
||||
REDIS=redis://:crs-9ltb97ds:i33dkxshh@10.10.4.8:6379/13
|
||||
REDIS=redis://:crs-9ltb97ds:i33dkxshh@10.10.4.8:6379
|
||||
|
||||
SUSU_HOST = http://10.10.3.10:8010
|
||||
|
68
doc/api.md
Normal file
68
doc/api.md
Normal file
@ -0,0 +1,68 @@
|
||||
|
||||
## 一. 说明
|
||||
|
||||
所有接口均需上传sessionid
|
||||
|
||||
通用返回JSON结构, 接口Response的数据结构说明只包含data部分
|
||||
``` JSON
|
||||
{
|
||||
"errcode": 0, //0:成功 2: 缺少必要参数(accountid, sessionid) 4: 帐号被封, 5: 帐号未找到 100: 所有未定义的错误
|
||||
"errmsg": "", //错误描述
|
||||
"data": {}, // 数据
|
||||
}
|
||||
```
|
||||
|
||||
## 二. 客户端接口列表
|
||||
|
||||
### 1. 上报消息订阅状态
|
||||
|
||||
1. Method: POST
|
||||
2. URI: /api/svr/:accountid/update_subscribe
|
||||
3. HOST: https://ghost.kingsome.cn
|
||||
|
||||
> 说明: 该接口有2个作用
|
||||
> 1: 给wx.requestSubscribeMessage获取到的用户同意的模板id增加发送次数
|
||||
> 2: 更新某模板id的lastCheck时间, 目的是告诉服务端, 相关记录当天不需要再发送了
|
||||
|
||||
| 字段 | 说明 |
|
||||
| -------- | -------------------------------------- |
|
||||
| accountid | 帐号id|
|
||||
|
||||
> POST参数
|
||||
|
||||
| 字段 | 说明 |
|
||||
| -------- | -------------------------------------- |
|
||||
| templateids | 模板id列表或者模板类型(jsub_xxx)列表 |
|
||||
| counts | 1或0组成的数组, 须和templateids数量一致, 1: 表示增加一次发送机会, 0: 只更新lastCheck的时间 |
|
||||
|
||||
3. Response: JSON
|
||||
|
||||
```js
|
||||
{}
|
||||
|
||||
```
|
||||
|
||||
### 2. 发送订阅消息
|
||||
|
||||
1. Method: POST
|
||||
2. URI: /api/svr/:accountid/send_subscribe_msg
|
||||
3. HOST: https://ghost.kingsome.cn
|
||||
|
||||
| 字段 | 说明 |
|
||||
| -------- | -------------------------------------- |
|
||||
| accountid | 帐号id|
|
||||
|
||||
> POST参数
|
||||
|
||||
| 字段 | 说明 |
|
||||
| -------- | -------------------------------------- |
|
||||
| type | 消息的类型(jsub_xxx) |
|
||||
|
||||
3. Response: JSON
|
||||
|
||||
```js
|
||||
{
|
||||
msgid: '发送成功后, 微信返回的消息id'
|
||||
}
|
||||
|
||||
```
|
57
doc/subscribe.md
Normal file
57
doc/subscribe.md
Normal file
@ -0,0 +1,57 @@
|
||||
#订阅消息流程
|
||||
|
||||
##1. 相关文档
|
||||
|
||||
[服务端发送接口](https://developers.weixin.qq.com/minigame/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html)
|
||||
|
||||
[前端接口](https://developers.weixin.qq.com/minigame/dev/api/open-api/subscribe-message/wx.requestSubscribeSystemMessage.html)
|
||||
|
||||
|
||||
##2. 微信mp后台选择相关模板消息
|
||||
mp.weixin.qq.com - 功能-订阅消息
|
||||
选择对应的模板, 获得 模板ID 和 消息格式
|
||||
|
||||
##3. 金蚕mp后台-游戏配置-普通配置
|
||||
增加 订阅消息配置(subscribe_cfg)
|
||||
格式如下:
|
||||
```json
|
||||
{
|
||||
"jsub_login": "wL3rEJ4zSbHpIiKX8ZPpO1z77dAOO8HdFbUZzfAh-VA",
|
||||
"jsub_sign": "37Te9WPEzd-nB65_KxwiAF00DucDRjb4JzlWQBXjtsY"
|
||||
}
|
||||
```
|
||||
jsub_login 指 离线收益消息
|
||||
jsub_sign 指 每日签到消息
|
||||
值为微信mp后台获取到的模板ID
|
||||
|
||||
##4. 金蚕mp后台-游戏配置-服务端配置
|
||||
|
||||
增加 订阅消息配置(subscribe_cfg)
|
||||
格式如下(消息格式取自微信mp后台):
|
||||
```json
|
||||
{
|
||||
"jsub_login": {
|
||||
"thing4": "离线奖励快满了, 快来领取吧",
|
||||
"date2": "$date"
|
||||
},
|
||||
"jsub_sign": {
|
||||
"thing5": "今天还未签到, 快来签到领取好礼吧",
|
||||
"date4": "$date"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##5. 客户端逻辑
|
||||
|
||||
客户端启动游戏时, 获取到相关订阅消息的模板id列表,
|
||||
通过wx.requestSubscribeMessage取得用户授权后, 将对应的模板id通过上报接口传给服务端
|
||||
|
||||
##6. 服务端逻辑
|
||||
|
||||
服务端暂时采用定时发送的机制, 到达指定时间后, 会遍历所有可发送的记录, 优先发送签到消息
|
||||
判断可发送的条件: count > 0 && lastCheck < 今天0点
|
||||
发送成功后, 会更新lastCheck,
|
||||
上报接口也会更新lastCheck的时间
|
||||
|
||||
|
||||
|
@ -6,6 +6,7 @@ import { mongoose } from '@typegoose/typegoose'
|
||||
import logger from 'logger/logger'
|
||||
import config from 'config/config'
|
||||
import { RedisClient } from './redis/RedisClient'
|
||||
import { GameInfoCache } from './cache/GameInfoCache'
|
||||
|
||||
const zReqParserPlugin = require('plugins/zReqParser')
|
||||
|
||||
@ -139,6 +140,11 @@ export class ApiServer {
|
||||
return payload
|
||||
})
|
||||
}
|
||||
|
||||
private async initCaches() {
|
||||
await new GameInfoCache().init()
|
||||
}
|
||||
|
||||
public async start() {
|
||||
let self = this
|
||||
return new Promise(async (resolve, reject) => {
|
||||
@ -147,6 +153,7 @@ export class ApiServer {
|
||||
self.registerRouter()
|
||||
self.setErrHandler()
|
||||
self.setFormatSend()
|
||||
await self.initCaches()
|
||||
this.server.listen({ port: config.api.port }, (err: any, address: any) => {
|
||||
if (err) {
|
||||
logger.log(err)
|
||||
|
49
src/cache/GameInfoCache.ts
vendored
Normal file
49
src/cache/GameInfoCache.ts
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
import { singleton } from '../decorators/singleton'
|
||||
import { GameInfo } from '../models/GameInfo'
|
||||
|
||||
export class GameMiniInfo {
|
||||
public appId: string
|
||||
public appSecret: string
|
||||
|
||||
constructor(appId: string, appSecret: string) {
|
||||
this.appId = appId
|
||||
this.appSecret = appSecret
|
||||
}
|
||||
}
|
||||
|
||||
@singleton
|
||||
export class GameInfoCache {
|
||||
private _cache: Map<[string, string], GameMiniInfo> = new Map()
|
||||
|
||||
public async init() {
|
||||
let games = await GameInfo.find({ deleted: false })
|
||||
for (let game of games) {
|
||||
const gameId = game.game_id
|
||||
for (let p of game.platforms) {
|
||||
const channel = p.platform.platform_id
|
||||
const appId = p.app_id
|
||||
const appSecret = p.app_secret
|
||||
this._cache.set([gameId, channel], new GameMiniInfo(appId, appSecret))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public async getInfo(channel: string, gameId: string) {
|
||||
let info = this._cache.get([gameId, channel])
|
||||
if (!info) {
|
||||
let games = await GameInfo.find({ deleted: false, game_id: gameId, 'platforms.platform.platform_id': channel })
|
||||
if (games?.length > 0) {
|
||||
let game = games[0]
|
||||
let platform = game.platforms.find(o => o.platform.platform_id === channel)
|
||||
if (platform) {
|
||||
info = new GameMiniInfo(platform.app_id, platform.app_secret)
|
||||
this._cache.set([gameId, channel], info)
|
||||
}
|
||||
}
|
||||
}
|
||||
return info
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ let baseConfig = {
|
||||
},
|
||||
|
||||
db_main: process.env.DB_MAIN,
|
||||
db_second: process.env.DB_SECOND,
|
||||
db_pikachu: process.env.DB_PIKACHU,
|
||||
redis: process.env.REDIS,
|
||||
susuHost: process.env.SUSU_HOST,
|
||||
}
|
||||
|
@ -1,10 +1,72 @@
|
||||
import { role, router } from '../decorators/router'
|
||||
import BaseController from '../common/base.controller'
|
||||
import { checkAccountId, parseGameAccountId } from '../utils/string.util'
|
||||
import { ZError } from '../common/ZError'
|
||||
import { SubscribeRecord } from '../models/SubscribeRecord'
|
||||
import { getSubscribeInfo } from '../services/jcfw.svr'
|
||||
import { sendSubscribeMessage } from '../services/subscribe.svr'
|
||||
import { isToday } from '../utils/time.util'
|
||||
|
||||
class SubscribeController extends BaseController {
|
||||
@role('anon')
|
||||
@router('post /api/svr/send_msg')
|
||||
async receive(req: any) {
|
||||
@router('post /api/svr/:accountId/send_subscribe_msg')
|
||||
async sendSubscribeMsg(req: any) {
|
||||
let { accountId, type } = req.params
|
||||
let record = await SubscribeRecord.findOne({ accountId, type })
|
||||
if (!record || record.count < 1) {
|
||||
throw new ZError(20, '没有可用的模板id')
|
||||
}
|
||||
if (isToday(record.lastCheck)) {
|
||||
throw new ZError(21, '今天不需要发送')
|
||||
}
|
||||
const result = await sendSubscribeMessage({ accountId, type })
|
||||
record.lastCheck = Date.now()
|
||||
record.lastSend = Date.now()
|
||||
await record.save()
|
||||
return { msgid: result.msgid }
|
||||
}
|
||||
|
||||
@role('anon')
|
||||
@router('post /api/svr/:accountId/update_subscribe')
|
||||
async updateSubscribeStata(req: any) {
|
||||
let { accountId, templateids, counts } = req.params
|
||||
if (!accountId || !templateids) {
|
||||
throw new ZError(11, 'params mismatch')
|
||||
}
|
||||
if (!checkAccountId(accountId)) {
|
||||
throw new ZError(10, 'accountId error')
|
||||
}
|
||||
counts = counts || []
|
||||
let { gameId, channel, openId } = parseGameAccountId(accountId)
|
||||
for (let i = 0, l = templateids.length; i < l; i++) {
|
||||
const templateId = templateids[i]
|
||||
let record
|
||||
if (templateId.indexOf('jsub_') >= 0) {
|
||||
const cfg = await getSubscribeInfo({ gameId, channel, type: templateId })
|
||||
if (cfg.type && cfg.templateId) {
|
||||
record = await SubscribeRecord.insertOrUpdate(
|
||||
{ gameId, channel, accountId: accountId, templateId: cfg.templateId, type: cfg.type },
|
||||
{},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const cfg = await getSubscribeInfo({ gameId, channel, templateId })
|
||||
if (cfg.type && cfg.templateId) {
|
||||
record = await SubscribeRecord.insertOrUpdate(
|
||||
{ gameId, channel, accountId: accountId, templateId: cfg.templateId, type: cfg.type },
|
||||
{},
|
||||
)
|
||||
}
|
||||
}
|
||||
if (record) {
|
||||
const count = counts[i] || 0
|
||||
if (count > 0) {
|
||||
record.count += 1
|
||||
}
|
||||
record.lastCheck = Date.now()
|
||||
await record.save()
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
43
src/models/GameInfo.ts
Normal file
43
src/models/GameInfo.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { BaseModule } from './Base'
|
||||
import { getModelForClass, modelOptions, prop } from '@typegoose/typegoose'
|
||||
import { dbconn } from '../decorators/dbconn'
|
||||
|
||||
class Platform {
|
||||
@prop()
|
||||
public platform_id: string
|
||||
@prop()
|
||||
public name: string
|
||||
}
|
||||
|
||||
class GamePlatform {
|
||||
@prop()
|
||||
public app_id: string
|
||||
@prop()
|
||||
public app_secret: string
|
||||
@prop()
|
||||
public platform: Platform
|
||||
}
|
||||
@dbconn('pikachu')
|
||||
@modelOptions({ schemaOptions: { collection: 'game_info', timestamps: true } })
|
||||
class GameInfoClass extends BaseModule {
|
||||
@prop({ required: true, alias: 'gameName' })
|
||||
public game_name: string
|
||||
@prop({ alias: 'gameNameEn' })
|
||||
public game_name_en: string
|
||||
@prop({ alias: 'gameId' })
|
||||
public game_id: string
|
||||
@prop({ alias: 'gameType' })
|
||||
public game_type: string
|
||||
@prop({ default: '512*1024', alias: 'gameSize' })
|
||||
public game_size: string
|
||||
@prop({ alias: 'gameIcon' })
|
||||
public game_icon: string
|
||||
@prop({ alias: 'appKey' })
|
||||
public app_key: string
|
||||
@prop({ type: () => [GamePlatform] })
|
||||
public platforms: GamePlatform[]
|
||||
@prop()
|
||||
public deleted: boolean
|
||||
}
|
||||
|
||||
export const GameInfo = getModelForClass(GameInfoClass, { existingConnection: GameInfoClass.db })
|
38
src/models/SubscribeHistory.ts
Normal file
38
src/models/SubscribeHistory.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { BaseModule } from './Base'
|
||||
import { dbconn } from '../decorators/dbconn'
|
||||
import { getModelForClass, modelOptions, prop, Severity, mongoose } from '@typegoose/typegoose'
|
||||
import { parseGameAccountId } from '../utils/string.util'
|
||||
|
||||
@dbconn()
|
||||
@modelOptions({
|
||||
schemaOptions: { collection: 'subscribe_history', timestamps: true },
|
||||
options: { allowMixed: Severity.ALLOW },
|
||||
})
|
||||
class SubscribeHistoryClass extends BaseModule {
|
||||
@prop()
|
||||
public gameId: string
|
||||
@prop()
|
||||
public channel: string
|
||||
@prop()
|
||||
public accountId: string
|
||||
@prop()
|
||||
public templateId: string
|
||||
|
||||
@prop({ type: mongoose.Schema.Types.Mixed })
|
||||
public result: any
|
||||
|
||||
public static async log(accountId: string, templateId: string, result: any) {
|
||||
let record = new SubscribeHistory()
|
||||
record.accountId = accountId
|
||||
record.templateId = templateId
|
||||
record.result = result
|
||||
let { gameId, channel } = parseGameAccountId(accountId)
|
||||
record.gameId = gameId
|
||||
record.channel = channel
|
||||
await record.save()
|
||||
}
|
||||
}
|
||||
|
||||
export const SubscribeHistory = getModelForClass(SubscribeHistoryClass, {
|
||||
existingConnection: SubscribeHistoryClass.db,
|
||||
})
|
35
src/models/SubscribeRecord.ts
Normal file
35
src/models/SubscribeRecord.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { BaseModule } from './Base'
|
||||
import { dbconn } from '../decorators/dbconn'
|
||||
import { getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose'
|
||||
|
||||
@dbconn()
|
||||
@index({ accountId: 1 }, { unique: false })
|
||||
@index({ templateId: 1 }, { unique: false })
|
||||
@index({ type: 1 }, { unique: false })
|
||||
@index({ accountId: 1, templateId: 1 }, { unique: true })
|
||||
@index({ accountId: 1, type: 1 }, { unique: true })
|
||||
@modelOptions({ schemaOptions: { collection: 'subscribe_record', timestamps: true } })
|
||||
class SubscribeRecordClass extends BaseModule {
|
||||
@prop()
|
||||
public gameId: string
|
||||
@prop()
|
||||
public channel: string
|
||||
@prop()
|
||||
public accountId: string
|
||||
@prop()
|
||||
public templateId: string
|
||||
/**
|
||||
* 定义好的类型, 以 jsub_ 开头
|
||||
* @type {string}
|
||||
*/
|
||||
@prop()
|
||||
public type: string
|
||||
@prop({ default: 0 })
|
||||
public count: number
|
||||
@prop({ default: 0 })
|
||||
public lastSend: number
|
||||
@prop({ default: 0 })
|
||||
public lastCheck: number
|
||||
}
|
||||
|
||||
export const SubscribeRecord = getModelForClass(SubscribeRecordClass, { existingConnection: SubscribeRecordClass.db })
|
33
src/schedule/subscribe.schedule.ts
Normal file
33
src/schedule/subscribe.schedule.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { singleton } from '../decorators/singleton'
|
||||
import * as schedule from 'node-schedule'
|
||||
import { SubscribeRecord } from '../models/SubscribeRecord'
|
||||
import { todayStart } from '../utils/time.util'
|
||||
import { sendSubscribeMessage } from '../services/subscribe.svr'
|
||||
|
||||
@singleton
|
||||
export default class SubscribeSchedule {
|
||||
async parseAllRecord() {
|
||||
let records = await SubscribeRecord.find({ count: { $gt: 0 }, lastCheck: { $lt: todayStart() } })
|
||||
let sendSet: Set<string> = new Set()
|
||||
for (let record of records) {
|
||||
if (sendSet.has(record.accountId)) {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
await sendSubscribeMessage({ accountId: record.accountId, type: record.type })
|
||||
record.lastCheck = Date.now()
|
||||
record.lastSend = Date.now()
|
||||
record.count -= 1
|
||||
sendSet.add(record.accountId)
|
||||
await record.save()
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
scheduleAll() {
|
||||
const job = schedule.scheduleJob('1 0 20 * * *', async () => {
|
||||
await this.parseAllRecord()
|
||||
})
|
||||
}
|
||||
}
|
102
src/services/jcfw.svr.ts
Normal file
102
src/services/jcfw.svr.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { RedisClient } from '../redis/RedisClient'
|
||||
import exp from 'constants'
|
||||
|
||||
/**
|
||||
* 获取某游戏的所有客户端配置
|
||||
* @param {string} gameId
|
||||
* @param {string} channel
|
||||
* @return {Promise<{key: string, value: string}[]>}
|
||||
*/
|
||||
export async function getGameClientCfg(gameId: string, channel: string) {
|
||||
const key = `config:${gameId}:${channel}`
|
||||
const records = await new RedisClient().get(key)
|
||||
let results: { key: string; value: string }[] = []
|
||||
if (records) {
|
||||
results = JSON.parse(records as string)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
export async function getGameSvrCfg(gameId: string, channel: string) {
|
||||
const key = `server_config:${gameId}:${channel}`
|
||||
const records = await new RedisClient().get(key)
|
||||
let results: { key: string; value: string }[] = []
|
||||
if (records) {
|
||||
results = JSON.parse(records as string)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取游戏的订阅消息配置
|
||||
* @param {string} gameId
|
||||
* @param {string} channel
|
||||
* @param svr 是否是服务端配置
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
export async function getSubscribeCfg(gameId: string, channel: string, svr: boolean = false) {
|
||||
const cfgs = svr ? await getGameSvrCfg(gameId, channel) : await getGameClientCfg(gameId, channel)
|
||||
const result: any = {}
|
||||
for (let i = 0, l = cfgs.length; i < l; i++) {
|
||||
if (cfgs[i].key === 'subscribe_cfg') {
|
||||
const val = JSON.parse(cfgs[i].value)
|
||||
Object.assign(result, val)
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据type获取对应的订阅消息模板id
|
||||
* @param {string} gameId
|
||||
* @param {string} channel
|
||||
* @param {string} type
|
||||
* @param {string} templateId
|
||||
* @return {Promise<{type: string, templateId: string}>}
|
||||
*/
|
||||
export async function getSubscribeInfo({
|
||||
gameId,
|
||||
channel,
|
||||
type,
|
||||
templateId,
|
||||
}: {
|
||||
gameId: string
|
||||
channel: string
|
||||
type?: string
|
||||
templateId?: string
|
||||
}) {
|
||||
const cfg = await getSubscribeCfg(gameId, channel)
|
||||
if (type) {
|
||||
templateId = cfg[type]
|
||||
} else if (templateId) {
|
||||
for (let key of Object.keys(cfg)) {
|
||||
if (cfg[key] === templateId) {
|
||||
type = key
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return { type, templateId }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订阅消息的消息模板
|
||||
* @param {string} gameId
|
||||
* @param {string} channel
|
||||
* @param {string} type
|
||||
*/
|
||||
export async function getSubscribeMsg(gameId: string, channel: string, type: string) {
|
||||
const cfg = await getSubscribeCfg(gameId, channel, true)
|
||||
const data = cfg[type]
|
||||
if (data) {
|
||||
for (let key of Object.keys(data)) {
|
||||
if (data[key].indexOf('$date') > -1) {
|
||||
data[key] = { value: data[key].replace('$date', new Date().format('yyyy-MM-dd')) }
|
||||
} else {
|
||||
data[key] = { value: data[key] }
|
||||
}
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
51
src/services/subscribe.svr.ts
Normal file
51
src/services/subscribe.svr.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { checkAccountId, parseGameAccountId } from '../utils/string.util'
|
||||
import { ZError } from '../common/ZError'
|
||||
import { GameInfoCache } from '../cache/GameInfoCache'
|
||||
import { getSubscribeInfo, getSubscribeMsg } from './jcfw.svr'
|
||||
import { sendWxSubscribeMessage } from './wechat/Wechat'
|
||||
import { SubscribeHistory } from '../models/SubscribeHistory'
|
||||
|
||||
export async function sendSubscribeMessage({
|
||||
accountId,
|
||||
type,
|
||||
templateId,
|
||||
}: {
|
||||
accountId: string
|
||||
type?: string
|
||||
templateId?: string
|
||||
}) {
|
||||
if (!checkAccountId(accountId)) {
|
||||
throw new ZError(10, 'accountId error')
|
||||
}
|
||||
let { gameId, channel, openId } = parseGameAccountId(accountId)
|
||||
if (channel !== '6001') {
|
||||
throw new ZError(12, 'this platform can`t send subscribe msg')
|
||||
}
|
||||
if (!type && !templateId) {
|
||||
throw new ZError(13, 'no msg type specify')
|
||||
}
|
||||
let info = await new GameInfoCache().getInfo(channel, gameId)
|
||||
if (!info) {
|
||||
throw new ZError(11, 'game cfg not found')
|
||||
}
|
||||
const cfg = await getSubscribeInfo({ gameId, channel, type, templateId })
|
||||
if (!cfg.type || !cfg.templateId) {
|
||||
throw new ZError(14, 'subscribe cfg not found')
|
||||
}
|
||||
const msgData = await getSubscribeMsg(gameId, channel, cfg.type)
|
||||
if (!msgData) {
|
||||
throw new ZError(15, 'msg cfg not found')
|
||||
}
|
||||
const result = await sendWxSubscribeMessage({
|
||||
appId: info.appId,
|
||||
appSecret: info.appSecret,
|
||||
openId,
|
||||
templateId: cfg.templateId,
|
||||
data: msgData,
|
||||
})
|
||||
await SubscribeHistory.log(accountId, cfg.templateId, result.data)
|
||||
if (result?.data?.errcode) {
|
||||
throw new ZError(result.data.errcode, result.data.errmsg)
|
||||
}
|
||||
return result.data
|
||||
}
|
@ -14,7 +14,6 @@ const URL_GENERATE_SCHEMA = 'https://api.weixin.qq.com/wxa/generatescheme'
|
||||
* @param {string} appId
|
||||
* @param {string} appSecret
|
||||
* @param {boolean} refresh 是否强制刷新
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
export async function refreshToken(appId: string, appSecret: string, refresh: boolean = false) {
|
||||
const util = new WxTokenCache()
|
||||
@ -143,7 +142,7 @@ export async function msgSecCheck(content: string) {
|
||||
* @param templateId
|
||||
* @param data
|
||||
*/
|
||||
export async function sendSubscribeMessage({ appId, appSecret, openId, templateId, data }) {
|
||||
export async function sendWxSubscribeMessage({ appId, appSecret, openId, templateId, data }) {
|
||||
const params = { touser: openId, template_id: templateId, data }
|
||||
const token = await refreshToken(appId, appSecret)
|
||||
const url = `${URL_SUBSCRIBE_MSG}?access_token=${token}`
|
||||
@ -154,7 +153,7 @@ export async function sendSubscribeMessage({ appId, appSecret, openId, templateI
|
||||
'Cache-Control': 'no-cache',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: params,
|
||||
data: JSON.stringify(params),
|
||||
}
|
||||
return axios(reqConfig)
|
||||
}
|
||||
|
@ -120,7 +120,7 @@ export function isJsonString(str: string) {
|
||||
}
|
||||
|
||||
export function checkAccountId(accountId: string) {
|
||||
return /^\d{4}_\d{4}_.+$/.test(accountId)
|
||||
return /^\d{4}_\d{4,6}_.+$/.test(accountId)
|
||||
}
|
||||
export function parseGameAccountId(accountId: string) {
|
||||
const arr = accountId.split('_')
|
||||
|
@ -1,16 +1,42 @@
|
||||
export const ONE_DAY_MILLISECOND = 1000 * 3600 * 24
|
||||
/**
|
||||
* 获取n天前的time
|
||||
* @param {number} day
|
||||
* @return {number}
|
||||
*/
|
||||
export function timeBeforeDay(day: number): number {
|
||||
let time = Date.now();
|
||||
return time - day * 1000 * 24 * 24;
|
||||
let time = Date.now()
|
||||
return time - day * ONE_DAY_MILLISECOND
|
||||
}
|
||||
|
||||
//间隔天数
|
||||
export function calcBetweenDays(time1: number, time2: number) {
|
||||
let v1 = Math.floor(time1/3600/24/1000);
|
||||
let v2 = Math.floor(time2/3600/24/1000);
|
||||
return Math.abs(v1 - v2);
|
||||
let v1 = Math.floor(time1 / ONE_DAY_MILLISECOND)
|
||||
let v2 = Math.floor(time2 / ONE_DAY_MILLISECOND)
|
||||
return Math.abs(v1 - v2)
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否是今天
|
||||
* @param {number} time
|
||||
* @return {boolean}
|
||||
*/
|
||||
export function isToday(time: number) {
|
||||
return new Date().toDateString() === new Date(time).toDateString()
|
||||
}
|
||||
|
||||
/**
|
||||
* 今天开始的时间
|
||||
* @return {number}
|
||||
*/
|
||||
export function todayStart() {
|
||||
return new Date(new Date().toLocaleDateString()).getTime()
|
||||
}
|
||||
|
||||
/**
|
||||
* 今天结束的时间
|
||||
* @return {number}
|
||||
*/
|
||||
export function todayEnd() {
|
||||
return todayStart() + ONE_DAY_MILLISECOND - 1
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user