Commit 3bdb9585 authored by qinhaitao's avatar qinhaitao

feat: 🎸 游戏api

parent f58b6761
......@@ -71,6 +71,7 @@ class BaseDao {
query: query || {},
options: options || {}
})
console.log(result)
let res = result.nModified > 0 ? 1 : 0
console.log(`update结果`, res)
return res
......
......@@ -8,10 +8,9 @@
"sdkVersion": "*",
"dependencies": {
"dayjs": "^1.8.28",
"get-value": "^3.0.1",
"lodash": "^4.17.20",
"mpath": "^0.7.0",
"node-xlsx": "^0.15.0",
"set-value": "^3.0.2",
"source-map-support": "^0.5.19",
"taobao-mini-sdk": "^0.2.4"
},
......
/** @format */
import { services, checkParams } from '../decorator/common'
import { services, checkParams, preCheck, preUpdate } from '../decorator/common'
import { resultsModel } from '../sdk'
import { UserService } from '../service'
import { UserService, GameService } from '../service'
import { checkActivityTime, checkJoinId, checkUserInfo, checkVip } from '../utils/common/check'
import { updateUserInfo } from '../utils/common/update'
export default class User {
export interface ISumitGameControllerInfos extends IControllerInfos {
joinInfo: IJoinRecord
}
export default class Game {
/**
* 获取游戏信息
*
* needKeys 根据需求自取字段 格式: { gameTimes: 1}
* needKeys 根据需求自取字段 格式: { gameTimes: 1 }
*/
@checkParams(['activityId', 'needKeys'])
@services([UserService])
......@@ -23,4 +29,39 @@ export default class User {
...gameInfoResult
})
}
@checkParams(['activityId'])
@services([GameService])
@preCheck([checkActivityTime, checkVip, checkUserInfo({ gameTimes: { $gte: 3 } }, '游戏次数不足,做点任务吧')])
@preUpdate([
updateUserInfo({
$where: 'this.gameTimes >3',
$inc: { gameTimes: -3 }
})
])
async startGame(
context: IContext<IParams>,
{ userInfo, activityInfo }: IControllerInfos,
[gameService]: [GameService]
) {
const joinResult = await gameService.addJoinRecord(userInfo)
return resultsModel.success({
...joinResult
})
}
@checkParams(['activityId', 'id', 'score'])
@services([GameService])
@preCheck([checkJoinId])
async submitGame(
context: IContext<IParams>,
{ userInfo, joinInfo }: ISumitGameControllerInfos,
[gameService]: [GameService]
) {
const { id, score } = context.data
const result = await gameService.submitGame(id, score, joinInfo)
return resultsModel.success(result)
}
}
......@@ -4,7 +4,7 @@ import { services, checkParams, registeInfos, preUpdate } from '../decorator/com
import { resultsModel } from '../sdk'
import { UserService, AccessService } from '../service'
import { noCheckUser } from '../decorator/common'
export default class User {
export default class LoginController {
/**
* 登录接口, 初始化/更新用户信息
*
......
/** @format */
export const ACCESS_DB_NAME: string = 'c_user_access'
export const ACCESS_DB_NAME = 'c_user_access'
export const USER_DB_NAME: string = 'c_user'
export const USER_DB_NAME = 'c_user'
export const AWARDS_DB_NAME: string = 'c_awards_info'
export const AWARDS_DB_NAME = 'c_awards_info'
export const STAT_DB_NAME: string = 'c_stats'
export const JOIN_DB_NAME = 'c_user_join'
export const ERROR_LOG_DB_NAME: string = 'error_log'
export const STAT_DB_NAME = 'c_stats'
export const SELLER_INFO_DB_NAME: string = 'a_seller_info'
export const ERROR_LOG_DB_NAME = 'error_log'
export const PRIZE_CONFIG_DB_NAME: string = 'b_prize_config'
export const SELLER_INFO_DB_NAME = 'a_seller_info'
export const ACTIVITY_CONFIG_DB_NAME: string = 'b_activity_config'
export const PRIZE_CONFIG_DB_NAME = 'b_prize_config'
export const ACTIVITY_CONFIG_DB_NAME = 'b_activity_config'
......@@ -5,7 +5,9 @@ import { UserService } from '../../service'
import { recordErrorLog } from '../../utils/common/logger'
import { resultsModel } from '../../sdk'
import { CODE_TYPES } from '../../errorCode'
import { formatUpdatedDataByProjection } from '../../utils/common/format'
import { formatUpdatedDataByProjection, formatUpdateUserProjection } from '../../utils/common/format'
import { dbUpdate } from '../../utils/common/db'
import { USER_DB_NAME } from '../../db'
// 检验参数是否存在
export default function preUpdate(checks: IFunction[]) {
......@@ -32,7 +34,8 @@ export default function preUpdate(checks: IFunction[]) {
// 更新数据
try {
await preUpdateUser(context, otherArgs.userInfo, totalUpdateProjection)
const updateResult = await preUpdateUser(context, otherArgs.userInfo, totalUpdateProjection)
if (!updateResult) return resultsModel.error(CODE_TYPES.SYSTEM_ERROR, '用户信息更新失败')
// 避免再次查询
otherArgs.userInfo = formatUpdatedDataByProjection(otherArgs.userInfo, totalUpdateProjection)
} catch (error) {
......@@ -47,17 +50,32 @@ export default function preUpdate(checks: IFunction[]) {
async function preUpdateUser(context: IContext<IParams>, userInfo: IUserInfo, updateProjection: IPreUpdateQuery) {
// 删除空的操作
if (isEmpty(updateProjection.$inc)) {
if (isEmpty(updateProjection?.$inc)) {
delete updateProjection.$inc
}
if (isEmpty(updateProjection.$set)) {
if (isEmpty(updateProjection?.$set)) {
delete updateProjection.$set
}
if (isEmpty(updateProjection.$push)) {
if (isEmpty(updateProjection?.$push)) {
delete updateProjection.$push
}
if (isEmpty(updateProjection?.$where)) {
delete updateProjection.$where
}
if (isEmpty(updateProjection)) return true
const userService = new UserService(context)
return await userService.updateUser(userInfo._id, updateProjection)
const query = updateProjection?.$where
? {
_id: userInfo?._id,
$where: updateProjection?.$where
}
: {
_id: userInfo?._id
}
if (updateProjection?.$where) {
delete updateProjection?.$where
}
return await dbUpdate(context, USER_DB_NAME, query, formatUpdateUserProjection(updateProjection))
}
......@@ -33,6 +33,11 @@ export const BusinessError = {
code: `310003`,
defaultMsg: '非店铺会员'
},
// 未关注店铺
ERROR_NO_FOLLOW: {
code: `310004`,
defaultMsg: '未关注店铺'
},
// 暂无次数可领取
ERROR_TASK_NORECEIVE: {
code: `430001`,
......
......@@ -12,7 +12,7 @@ import GameController from './controller/game.controller'
const { getVipInfo, getRankList } = new UserController()
const { login } = new LoginController()
const { getGameInfo } = new GameController()
const { getGameInfo, startGame } = new GameController()
const {
getTaskList,
receiveTaskRewards,
......@@ -42,5 +42,6 @@ export default {
getStats,
addStat,
getRankList,
getGameInfo
getGameInfo,
startGame
}
/**
* 基本信息
*
* @format
*/
import { BaseDao, TBAPIS } from '../sdk'
import { JOIN_DB_NAME } from '../db'
import { ACTIVITY_STATUS } from '../constants'
import { getToday } from '../utils'
export default class GameService {
context: IContext<IParams>
joindao: IBaseDao
constructor(context: IContext<IParams>) {
this.context = context
this.joindao = new BaseDao(context, JOIN_DB_NAME)
}
async addJoinRecord(userInfo: IUserInfo) {
const {
openId,
data: { activityId }
} = this.context
const { userNick, avatar } = userInfo
const now = Date.now()
const today = getToday()
const record: IJoinRecord = {
activityId,
userNick,
avatar,
openId,
createTime: now,
updateTime: now,
score: 0,
createDay: today
}
const id = await this.joindao.insertOne(record)
return { id }
}
async submitGame(id: string, score: number, joinInfo: IJoinRecord) {
const now = Date.now()
const today = getToday()
const duration = now - joinInfo.createTime
const playInfo = {
score,
duration,
submitTime: now,
updateTime: now,
submitDay: today
}
await this.joindao.update(
{
_id: id
},
{
$set: playInfo
}
)
return {
score,
duration
}
}
}
......@@ -6,3 +6,4 @@ export { default as UserService } from './user.service'
export { default as AwardsService } from './awards.service'
export { default as TaskService } from './task.service'
export { default as StatService } from './stat.service'
export { default as GameService } from './game.service'
......@@ -63,6 +63,9 @@ export default class StatService {
// 新增UV
const newUV = await this.userdao.count({ activityId, createDay: day })
// 新增UV(通过邀请)
const newUVFromInviteUV = await this.userdao.count({ activityId, createDay: day, inviteId: { $exists: true } })
// 已入会(老会员)PV
const vipPV = await this.accessdao.count({
activityId,
......@@ -101,6 +104,15 @@ export default class StatService {
const newFollowUV = await this.userdao.count({ activityId, 'follow.newFollow': true, 'follow.followDay': day })
// 助力成功UV
const helpSuccessUV = (
await this.statdao.aggregate([
{ $match: { activityId, createDay: day, type: STAT_TYPE.INITE_SUCCESS } },
{ $project: { openId: true } },
{ $group: { _id: '$openId', count: { $sum: 1 } } }
])
).length
// 根据任务类型获取完成任务的人数
// example: await getTaskCompleteUV('collectGoods', day)
const getTaskCompleteUV = async (task: string, day: string) => {
......@@ -115,12 +127,14 @@ export default class StatService {
访问PV: PV,
访问UV: UV,
新增UV: newUV,
'新增UV(通过邀请)': newUVFromInviteUV,
'已入会(老会员)PV': vipPV,
'已入会(老会员)UV': vipUV,
未入会PV: noVipPV,
未入会UV: noVipUV,
新入会UV: newVipUV,
新增关注UV: newFollowUV
新增关注UV: newFollowUV,
助力成功UV: helpSuccessUV
//收藏商品任务完成UV: await getTaskCompleteUV('collectGoods', day)
}
console.log(keyValueMapper, 'xlsxData')
......
......@@ -122,6 +122,7 @@ interface IPreUpdateQuery {
$inc?: { [key: string]: number | undefined }
$set?: { [key: string]: any }
$push?: { [key: string]: any }
$where?: string
}
interface IFindProjection {
......
/** @format */
interface IJoinRecord {
_id?: string
activityId: string
userNick: string
avatar: string
openId: string
createTime: number
updateTime: number
score: number
createDay: string
duration?: number
submitTime?: number
}
/** @format */
import { resultsModel } from '../../../sdk'
import { CODE_TYPES } from '../../../errorCode'
import { isBoolean } from 'lodash'
export default async function checkFollow(context: IContext<IParams>) {
const { isFollow } = context.data
if (!isBoolean(isFollow)) return resultsModel.error(CODE_TYPES.PARAMS_ERROR, '缺少isFollow参数')
if (!isFollow) {
return resultsModel.error(CODE_TYPES.ERROR_NO_FOLLOW)
}
}
/** @format */
import { resultsModel } from '../../../sdk'
import { CODE_TYPES } from '../../../errorCode'
import { dbFindOne } from '../db'
import { JOIN_DB_NAME } from '../../../db'
export default async function checkJoinId(context: IContext<IParams>) {
const { openId } = context
const { id, activityId } = context.data
if (!id) {
return resultsModel.error(CODE_TYPES.PARAMS_ERROR, `缺少id参数`)
}
const joinInfo = await dbFindOne<IJoinRecord>(context, JOIN_DB_NAME, { _id: id, activityId })
if (joinInfo?.submitTime) return resultsModel.error(CODE_TYPES.PARAMS_ERROR, `重复提交游戏!`)
if (joinInfo?.openId !== openId) return resultsModel.error(CODE_TYPES.PARAMS_ERROR, `无权限提交!`)
if (!joinInfo) return resultsModel.error(CODE_TYPES.SYSTEM_ERROR, '本局id不存在')
return {
joinInfo
}
}
/** @format */
import { isBoolean, isNumber, isObject, isString, isUndefined } from 'lodash'
import { get } from 'mpath'
import { CODE_TYPES } from '../../../errorCode'
import { resultsModel } from '../../../sdk'
export interface IGameLimitCondition {
$eq?: any
$gt?: any
$gte?: any
$in?: any[]
$lt?: any
$lte?: any
$ne?: any
}
export interface IGameLimit {
[params: string]: string | boolean | number | IGameLimitCondition
}
export default function checkUserInfo(gameLimit: IGameLimit, errorMsg: string) {
return async (context: IContext<{ activityId: string }>, { userInfo }: IControllerInfos) => {
console.log(gameLimit)
const allValid = validateData(userInfo, gameLimit)
if (!allValid) return resultsModel.error(CODE_TYPES.SYSTEM_ERROR, errorMsg)
console.log(allValid)
// const { id } = context.data
}
}
function validateData(data: any = {}, condition: IGameLimit) {
let allValid = true
Object.keys(condition).forEach(key => {
const value = condition?.[key]
if (isString(value) || isBoolean(value) || isNumber(value)) {
allValid = validateEq(data, key, value)
}
if (isObject(value)) {
!isUndefined(value?.$eq) && (allValid = validateEq(data, key, value?.$eq))
!isUndefined(value?.$gt) && (allValid = validateGt(data, key, value?.$gt))
!isUndefined(value?.$gte) && (allValid = validateGte(data, key, value?.$gte))
!isUndefined(value?.$lte) && (allValid = validateLte(data, key, value?.$lte))
!isUndefined(value?.$lt) && (allValid = validateLt(data, key, value?.$lt))
!isUndefined(value?.$in) && (allValid = validateIn(data, key, value?.$in))
!isUndefined(value?.$ne) && (allValid = validateNe(data, key, value?.$ne))
}
})
return allValid
}
function validateEq(data: any = {}, key: string, value: any) {
return get(key, data) === value
}
function validateGt(data: any = {}, key: string, value: any) {
return get(key, data) > value
}
function validateGte(data: any = {}, key: string, value: any) {
return get(key, data) >= value
}
function validateLt(data: any = {}, key: string, value: any) {
console.log(get(key, data))
return get(key, data) < value
}
function validateLte(data: any = {}, key: string, value: any) {
return get(key, data) <= value
}
function validateIn(data: any = {}, key: string, value: any[]) {
return value.includes(get(key, data))
}
function validateNe(data: any = {}, key: string, value: any) {
return get(key, data) !== value
}
......@@ -2,10 +2,13 @@
import { resultsModel } from '../../../sdk'
import { CODE_TYPES } from '../../../errorCode'
import { formatVipCbUrl, getShopVip } from '../vip'
export default async function checkVip(context: IContext<IParams>, { vipInfo }: IControllerInfos) {
export default async function checkVip(context: IContext<IParams>, { vipInfo, activityInfo }: IControllerInfos) {
if (!vipInfo) {
console.error(`使用checkVip registeInfos必须注册vipInfo`)
vipInfo = await getShopVip(context, activityInfo, formatVipCbUrl(context))
}
if (!vipInfo.isVip) return resultsModel.error(CODE_TYPES.ERROR_NO_VIP)
if (!vipInfo?.isVip) return resultsModel.error(CODE_TYPES.ERROR_NO_VIP)
return { vipInfo }
}
......@@ -9,6 +9,8 @@ import checkRemainTimes from './checkRemainTimes'
import checkTaskLimit from './checkTaskLimit'
import checkValidPrize, { checkValidEnamePrize, checkValidObjectPrize } from './checkValidPrize'
import checkVip from './checkVip'
import checkUserInfo from './checkUserInfo'
import checkJoinId from './checkJoinId'
const check = {
checkActivityTime,
......@@ -22,7 +24,9 @@ const check = {
checkValidPrize,
checkValidEnamePrize,
checkValidObjectPrize,
checkVip
checkVip,
checkUserInfo,
checkJoinId
}
export default check
......@@ -39,5 +43,7 @@ export {
checkValidPrize,
checkValidEnamePrize,
checkValidObjectPrize,
checkVip
checkVip,
checkUserInfo,
checkJoinId
}
/** @format */
import { assign, isEmpty, merge } from 'lodash'
import * as set from 'set-value'
import * as get from 'get-value'
import { set, get } from 'mpath'
/**
* updateUser projection 格式化
......@@ -49,22 +48,22 @@ export function formatUpdatedDataByProjection(dbData: any, projection: IPreUpdat
if (!isEmpty(projection.$set)) {
Object.keys(projection.$set).forEach(key => {
set(updatedDbData, key, projection.$set?.[key])
set(key, projection.$set?.[key], updatedDbData)
})
}
if (!isEmpty(projection.$inc)) {
Object.keys(projection.$inc).forEach(key => {
const originValue = get(updatedDbData, key) || 0
set(updatedDbData, key, projection.$inc?.[key] + originValue)
const originValue = get(key, updatedDbData) || 0
set(key, projection.$inc?.[key] + originValue, updatedDbData)
})
updatedDbData = merge({}, dbData, projection.$set)
}
if (!isEmpty(projection.$push)) {
Object.keys(projection.$push).forEach(key => {
const originValue = get(updatedDbData, key) || []
set(updatedDbData, key, [...originValue, projection.$push?.[key]])
const originValue = get(key, updatedDbData) || []
set(key, [...originValue, projection.$push?.[key], updatedDbData])
})
}
return updatedDbData
......
......@@ -3,14 +3,16 @@ import updateVip from './updateVip'
import updateSignTask from './updateSignTask'
import updateOrderGoods from './updateOrderGoods'
import updateFirstLoginToday from './updateFirstLoginToday'
import updateUserInfo from './updateUserInfo'
const update = {
updateVip,
updateSignTask,
updateOrderGoods,
updateFirstLoginToday
updateFirstLoginToday,
updateUserInfo
}
export default update
export { updateVip, updateSignTask, updateOrderGoods, updateFirstLoginToday }
export { updateVip, updateSignTask, updateOrderGoods, updateFirstLoginToday, updateUserInfo }
/** @format */
import { getToday } from '../getToday'
// login接口使用 当当天第一次登陆有数据变更的时候使用
// login接口使用 当天第一次登陆有数据变更的时候使用
// updateFirstLoginToday({ $inc: { gameTime: 3}})
export default function updateFirstLoginToday(update?: IPreUpdateQuery) {
return async function (
context: IContext<IParams>,
......
/** @format */
import { getToday } from '../getToday'
// login接口使用 当天第一次登陆有数据变更的时候使用
// updateFirstLoginToday({ $inc: { gameTime: 3}})
export default function updateUserInfo(update?: IPreUpdateQuery) {
return async function (
context: IContext<IParams>,
{ userInfo }: IControllerInfos
): Promise<IPreUpdateQuery | undefined> {
if (!userInfo) return {}
return update
}
}
......@@ -2,5 +2,5 @@
// 测试环境下 duiba2及其子账号自动开启VIP_MOCK, 返回数据可在此设置
export const VIP_MOCK = {
isVip: false
isVip: true
}
......@@ -22,6 +22,7 @@
"./src/**/*"
],
"exclude": [
"./src/controller/common/**"
"./src/controller/common/**",
"./dist/**/*"
]
}
\ No newline at end of file
......@@ -547,13 +547,6 @@ get-stdin@^6.0.0:
resolved "https://registry.npm.taobao.org/get-stdin/download/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b"
integrity sha1-ngm/cSs2CrkiXoEgSPcf3pyJZXs=
get-value@^3.0.1:
version "3.0.1"
resolved "https://registry.npm.taobao.org/get-value/download/get-value-3.0.1.tgz#5efd2a157f1d6a516d7524e124ac52d0a39ef5a8"
integrity sha1-Xv0qFX8dalFtdSThJKxS0KOe9ag=
dependencies:
isobject "^3.0.1"
glob-parent@^5.0.0:
version "5.1.1"
resolved "https://registry.npm.taobao.org/glob-parent/download/glob-parent-5.1.1.tgz?cache=0&sync_timestamp=1584836110944&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fglob-parent%2Fdownload%2Fglob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
......@@ -638,23 +631,11 @@ is-glob@^4.0.0, is-glob@^4.0.1:
dependencies:
is-extglob "^2.1.1"
is-plain-object@^2.0.4:
version "2.0.4"
resolved "https://registry.npm.taobao.org/is-plain-object/download/is-plain-object-2.0.4.tgz?cache=0&sync_timestamp=1599667338683&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fis-plain-object%2Fdownload%2Fis-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
integrity sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=
dependencies:
isobject "^3.0.1"
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.npm.taobao.org/isexe/download/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
isobject@^3.0.1:
version "3.0.1"
resolved "https://registry.npm.taobao.org/isobject/download/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.npm.taobao.org/js-tokens/download/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
......@@ -715,6 +696,11 @@ mkdirp@^0.5.1:
dependencies:
minimist "^1.2.5"
mpath@^0.7.0:
version "0.7.0"
resolved "https://registry.npm.taobao.org/mpath/download/mpath-0.7.0.tgz#20e8102e276b71709d6e07e9f8d4d0f641afbfb8"
integrity sha1-IOgQLidrcXCdbgfp+NTQ9kGvv7g=
ms@^2.1.1:
version "2.1.2"
resolved "https://registry.npm.taobao.org/ms/download/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
......@@ -823,13 +809,6 @@ semver@^7.2.1, semver@^7.3.2:
resolved "https://registry.npm.taobao.org/semver/download/semver-7.3.2.tgz?cache=0&sync_timestamp=1586886267748&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsemver%2Fdownload%2Fsemver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
integrity sha1-YElisFK4HtB4aq6EOJ/7pw/9OTg=
set-value@^3.0.2:
version "3.0.2"
resolved "https://registry.npm.taobao.org/set-value/download/set-value-3.0.2.tgz#74e8ecd023c33d0f77199d415409a40f21e61b90"
integrity sha1-dOjs0CPDPQ93GZ1BVAmkDyHmG5A=
dependencies:
is-plain-object "^2.0.4"
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.npm.taobao.org/shebang-command/download/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment