diff --git a/game-server/app/servers/battle/handler/auctionHandler.ts b/game-server/app/servers/battle/handler/auctionHandler.ts index b5e8481fa..54fc8f1f1 100644 --- a/game-server/app/servers/battle/handler/auctionHandler.ts +++ b/game-server/app/servers/battle/handler/auctionHandler.ts @@ -1,13 +1,14 @@ import { DividendModel } from './../../../db/Dividend'; import { Application, BackendSession, ChannelService } from "pinus"; -import { AUCTION_STAGE, DEBUG_MAGIC_WORD, STATUS, OFFER_RATIO, CURRENCY_BY_TYPE, CURRENCY_TYPE } from "../../../consts"; +import { AUCTION_STAGE, DEBUG_MAGIC_WORD, STATUS, OFFER_RATIO, CURRENCY_BY_TYPE, CURRENCY_TYPE, DATA_NAME, LOT_STATUS } from "../../../consts"; import { LotModel } from "../../../db/Lot"; import { ItemReward } from "../../../domain/dbGeneral"; import { resResult } from "../../../pubUtils/util"; -import { auctionBegin, auctionStage, calculateDividend, genAuction, todayGuildBegin } from "../../../services/auctionService"; +import { auctionStage, calculateDividend, genAuction, todayGuildBegin } from "../../../services/auctionService"; import { addItems, handleCost } from '../../../services/rewardService'; import { getSimpleRoleInfo } from '../../../services/roleService'; import { getRoleOnlineInfo } from '../../../services/redisService'; +import { lockData } from '../../../services/redLockService'; export default function (app: Application) { return new AuctionHandler(app); @@ -37,41 +38,79 @@ export class AuctionHandler { async offer(msg: { code: string, max: boolean }, session: BackendSession) { const { code, max } = msg; - let maxFlag = max; - const lot = await LotModel.findLot(code); - if (!lot) return resResult(STATUS.GUILD_LOT_NOT_FOUND); const roleId = session.get('roleId'); const roleName = session.get('roleName'); const sid = session.get('sid'); - const { curBuyer, curPrice, maxPrice, gid, count, bidRoles } = lot; - if (curBuyer === roleId) return resResult(STATUS.LOT_OFFER_SERIAL); - let newPrice = parseInt((curPrice * OFFER_RATIO).toFixed(0)); - if (newPrice >= maxPrice) { - newPrice = maxPrice; - maxFlag = true; - } - const costRes = await handleCost(roleId, sid, [{ id: CURRENCY_BY_TYPE.get(CURRENCY_TYPE.GOLD), count: newPrice }]); - if (!costRes) return resResult(STATUS.ROLE_COIN_NOT_ENOUGH); - const { roleName: buyerName } = await getSimpleRoleInfo(curBuyer); - const { sid: buyerSid } = await getRoleOnlineInfo(buyerName); - await addItems(curBuyer, buyerName, buyerSid, [{ id: CURRENCY_BY_TYPE.get(CURRENCY_TYPE.GOLD), count: curPrice }]); - // TODO 元宝返还纪录 - if (maxFlag) { - newPrice = maxPrice; - await addItems(roleId, roleName, sid, [{ id: gid, count }]); - } - bidRoles.push({roleId, price: newPrice, time: new Date()}); - const newLot = await LotModel.updateLot({ code, curBuyer: roleId, curPrice: newPrice, bidRoles}); + const serverId = session.get('serverId'); + let res: any = await lockData(serverId, DATA_NAME.AUCTION_LOT, code);// 加锁 - const incPrice = newPrice - (curBuyer ? curPrice : 0); - const dividend = await DividendModel.updateLot(code, gid, newPrice, incPrice); - const newDividend = await calculateDividend(dividend); - // TODO 加关注 - return resResult(STATUS.SUCCESS, { lot: newLot, dividend: newDividend }); + try { + if (!res) { + return resResult(STATUS.REDLOCK_ERR); + } + + const lot = await LotModel.findLot(code); + if (!lot || lot.status === LOT_STATUS.SOLD) { + res.releaseCallback(); + return resResult(STATUS.GUILD_LOT_NOT_FOUND); + } + + const { curBuyer, curPrice, maxPrice, gid, count, bidRoles, watchingRoles } = lot; + if (curBuyer === roleId) { + res.releaseCallback(); + return resResult(STATUS.LOT_OFFER_SERIAL); + } + let newPrice = parseInt((curPrice * OFFER_RATIO).toFixed(0)); + let maxFlag = max; + if (newPrice >= maxPrice) { + newPrice = maxPrice; + maxFlag = true; + } + const costRes = await handleCost(roleId, sid, [{ id: CURRENCY_BY_TYPE.get(CURRENCY_TYPE.GOLD), count: newPrice }]); + if (!costRes) { + res.releaseCallback(); + return resResult(STATUS.ROLE_COIN_NOT_ENOUGH); + } + + if (curBuyer) { + const { roleName: buyerName } = await getSimpleRoleInfo(curBuyer); + const { sid: buyerSid } = await getRoleOnlineInfo(buyerName); + await addItems(curBuyer, buyerName, buyerSid, [{ id: CURRENCY_BY_TYPE.get(CURRENCY_TYPE.GOLD), count: curPrice }]); + } + // TODO 元宝返还纪录 + if (maxFlag) { + newPrice = maxPrice; + await addItems(roleId, roleName, sid, [{ id: gid, count }]); + } + bidRoles.push({roleId, price: newPrice, time: new Date()}); + const newLot = await LotModel.updateLot({ code, curBuyer: roleId, curPrice: newPrice, bidRoles, status: maxFlag ? LOT_STATUS.SOLD : LOT_STATUS.ING, watchingRoles: Array.from(new Set([...watchingRoles, roleId])) }); + res.releaseCallback(); + + const incPrice = newPrice - (curBuyer ? curPrice : 0); + const dividend = await DividendModel.updateLot(code, gid, newPrice, incPrice); + const newDividend = await calculateDividend(dividend); + return resResult(STATUS.SUCCESS, { lot: newLot, dividend: newDividend }); + } catch (e) { + console.log('offer got err:', e); + res.releaseCallback(); + return resResult(STATUS.INTERNAL_ERR); + } } async watchLot(msg: { code: string }, session: BackendSession) { - return resResult(STATUS.SUCCESS); + const roleId = session.get('roleId'); + const { code } = msg; + const lot = await LotModel.watchLot(code, roleId); + if (!lot) return resResult(STATUS.WRONG_PARMS); + return resResult(STATUS.SUCCESS, { lot }); + } + + async unWatchLot(msg: { code: string }, session: BackendSession) { + const roleId = session.get('roleId'); + const { code } = msg; + const lot = await LotModel.unWatchLot(code, roleId); + if (!lot) return resResult(STATUS.WRONG_PARMS); + return resResult(STATUS.SUCCESS, { lot }); } async checkDividend(msg: {}, session: BackendSession) { diff --git a/game-server/app/services/auctionService.ts b/game-server/app/services/auctionService.ts index ca0c6bc0c..7c4473efa 100644 --- a/game-server/app/services/auctionService.ts +++ b/game-server/app/services/auctionService.ts @@ -1,5 +1,5 @@ import { DividendModel } from './../db/Dividend'; -import { LOT_CODE_LEN, AUCTION_STAGE, AUCTION_TIME, DIVIDEND_CODE_LEN, DIVIDEND_STATUS } from './../consts'; +import { LOT_CODE_LEN, AUCTION_STAGE, AUCTION_TIME, DIVIDEND_CODE_LEN, DIVIDEND_STATUS, LOT_STATUS } from './../consts'; import { DividendRec, ItemReward } from "../domain/dbGeneral"; import { genCode } from '../pubUtils/util'; import { LotModel, LotParam } from '../db/Lot'; @@ -64,14 +64,17 @@ export async function genAuction(guildCode: string, sourceType: number, sourceCo const code = genCode(LOT_CODE_LEN); return { auctionStage: AUCTION_STAGE.DEFAULT, sourceType, - sourceCode, serverId, guildCode, code, gid: id, count, begin, end, + sourceCode, serverId, guildCode, code, gid: id, count, begin, end, status: LOT_STATUS.DEFAULT, maxPrice: getMaxPrice(id, count), curPrice: getBasePrice(id, count), } }); const lots = await LotModel.createRecs(lotsData); const dividendCode = genCode(DIVIDEND_CODE_LEN); const dividendData: DividendParam = { - guildCode, sourceType, sourceCode, serverId, code: dividendCode, lots: [], dividends: [], totalPrice: 0, begin + guildCode, sourceType, sourceCode, serverId, code: dividendCode, dividends: [], totalPrice: 0, begin, lots: lots.map(lot => { + const { code, gid } = lot; + return { code, gid, price: 0 } + }), }; const dividend = await DividendModel.createDividend(dividendData); return { lots, dividend }; @@ -109,8 +112,9 @@ function weekendDividend(totalPrice: number, roleNum: number, date: Date) { } export async function calculateDividend(dividend: DividendType) { + if (!dividend) return null; const { code, guildCode, sourceType, sourceCode, lots, totalPrice, status, begin } = dividend; - if (status === DIVIDEND_STATUS.SENT) return null; + if (status === DIVIDEND_STATUS.SENT) return dividend; const calcuTotalPrice = lots.reduce((acc, lot) => { return acc + lot.price }, 0); if (calcuTotalPrice !== totalPrice) { await DividendModel.updateDividend(code, { totalPrice: calcuTotalPrice }); @@ -132,5 +136,5 @@ export async function calculateDividend(dividend: DividendType) { status: 0, // 0:未领取,1:已领取 }; }); - await DividendModel.updateDividend(code, { dividends }); + return await DividendModel.updateDividend(code, { dividends }); } diff --git a/game-server/test/auction.test.ts b/game-server/test/auction.test.ts index b4c818b9b..935e71458 100644 --- a/game-server/test/auction.test.ts +++ b/game-server/test/auction.test.ts @@ -4,6 +4,10 @@ import { PinusWSClient } from 'pinus-robot-plugin'; import { expect } from 'chai'; import { checkSuccessResponse } from './CheckPatten'; import { DEBUG_MAGIC_WORD, AUCTION_SOURCE } from '../app/consts'; +import { LOT_STATUS } from '../../shared/consts'; + +const GOOD_ID_TIEJIAN = 1; +const GOOD_ID_GANGJIAN = 2; function checkLot(lot) { expect(lot.auctionStage).to.be.a('number'); @@ -21,6 +25,31 @@ function checkLot(lot) { expect(Date.parse(lot.end)).to.be.a('number'); } +function checkLots(lots) { + for (let lot of lots) { + checkLot(lot); + } +} + +function checkDividend(dividend) { + expect(dividend.sourceType).to.be.a('number'); + expect(dividend.sourceCode).to.be.a('string'); + expect(dividend.serverId).to.be.a('number'); + expect(dividend.guildCode).to.be.a('string'); + expect(dividend.code).to.be.a('string'); + expect(dividend.status).to.be.a('number'); + expect(dividend.totalPrice).to.be.a('number'); + expect(dividend.lots).to.be.an('array'); + expect(dividend.dividends).to.be.an('array'); + expect(Date.parse(dividend.begin)).to.be.a('number'); +} + +function checkDividends(dividends) { + for (let dividend of dividends) { + checkDividend(dividend); + } +} + describe('拍卖行测试', function() { let pinusClient: PinusWSClient; let pinusClientT: PinusWSClient; // 用做测试队友 @@ -53,7 +82,7 @@ describe('拍卖行测试', function() { it('添加拍品', function(done) { const sourceCode = Math.random().toString(36).slice(-8); - pinusClient.request('battle.auctionHandler.debugAddLots', { magicWord: DEBUG_MAGIC_WORD, sourceType: AUCTION_SOURCE.GATE, sourceCode, rewards: [{ id: 1, count: 1 }, { id: 2, count: 2 }] }, (res) => { + pinusClient.request('battle.auctionHandler.debugAddLots', { magicWord: DEBUG_MAGIC_WORD, sourceType: AUCTION_SOURCE.GATE, sourceCode, rewards: [{ id: GOOD_ID_TIEJIAN, count: 1 }, { id: GOOD_ID_GANGJIAN, count: 2 }] }, (res) => { checkSuccessResponse(res); expect(res.data.lots).to.be.an('array'); for (let lot of res.data.lots) { @@ -65,35 +94,98 @@ describe('拍卖行测试', function() { it('获取拍卖行数据', function(done) { pinusClient.request('battle.auctionHandler.getAuction', {}, (res) => { - checkSuccessResponse(res, false); + checkSuccessResponse(res); + expect(res.data.lots).to.be.an('array'); + checkLots(res.data.lots); + expect(res.data.dividends).to.be.an('array'); + checkDividends(res.data.dividends); done(); }); }); - it('出价', function(done) { - pinusClient.request('battle.auctionHandler.offer', { code: '', max: false }, (res) => { - // checkSuccessResponse(res, false); - done(); + it('出价,竞价和一口价', function(done) { + let gid = null; + pinusClient.on('onEquipAdd', (res) => { + checkSuccessResponse(res); + expect(res.data.equipInfos).to.be.an('array'); + for (let info of res.data.equipInfos) { + expect(info.id).to.be.equal(gid); + } + }); + pinusClient.request('battle.auctionHandler.getAuction', {}, (res) => { + checkSuccessResponse(res); + expect(res.data.lots).to.be.an('array'); + checkLots(res.data.lots); + expect(res.data.dividends).to.be.an('array'); + checkDividends(res.data.dividends); + if (res.data.lots.length === 0) { + console.warn('没有拍品'); + done(); + return; + } + let unsoldLots = res.data.lots.filter(lot => {return lot.status !== LOT_STATUS.SOLD && lot.curBuyer !== roleInfo.roleId}); + if (unsoldLots.length === 0) { + console.warn('没有可出价的拍品'); + done(); + return; + } + pinusClient.request('battle.auctionHandler.offer', { code: unsoldLots[0].code, max: false }, (res) => { + checkSuccessResponse(res); + checkLot(res.data.lot); + checkDividend(res.data.dividend); + expect(res.data.lot.curBuyer).to.be.equal(roleInfo.roleId); + done(); + }); + unsoldLots.shift(); + if (unsoldLots.length === 0) { + console.warn('没有测试一口价的情况'); + done(); + return; + } + gid = unsoldLots[0].gid; + pinusClient.request('battle.auctionHandler.offer', { code: unsoldLots[0].code, max: true }, (res) => { + checkSuccessResponse(res); + checkLot(res.data.lot); + checkDividend(res.data.dividend); + expect(res.data.lot.curBuyer).to.be.equal(roleInfo.roleId); + setTimeout(() => { + done(); + }, 100); + }); }); }); - it('出一口价', function(done) { - pinusClient.request('battle.auctionHandler.offer', { code: '', max: true }, (res) => { - // checkSuccessResponse(res, false); - done(); - }); - }); - - it('关注', function(done) { - pinusClient.request('battle.auctionHandler.watchLot', { code: '' }, (res) => { - checkSuccessResponse(res, false); - done(); + it('关注后取关', function(done) { + pinusClient.request('battle.auctionHandler.getAuction', {}, (res) => { + checkSuccessResponse(res); + expect(res.data.lots).to.be.an('array'); + checkLots(res.data.lots); + expect(res.data.dividends).to.be.an('array'); + checkDividends(res.data.dividends); + if (res.data.lots.length === 0) { + console.warn('没有可关注的拍品'); + done(); + return; + } + const code = res.data.lots[0].code; + pinusClient.request('battle.auctionHandler.watchLot', { code }, (res) => { + checkSuccessResponse(res); + checkLot(res.data.lot); + expect(res.data.lot.watchingRoles.indexOf(roleInfo.roleId)).to.be.above(-1); + pinusClient.request('battle.auctionHandler.unWatchLot', { code }, (res) => { + checkSuccessResponse(res); + checkLot(res.data.lot); + expect(res.data.lot.watchingRoles.indexOf(roleInfo.roleId)).to.be.equal(-1); + done(); + }); + }); }); }); it('查看分红', function(done) { pinusClient.request('battle.auctionHandler.checkDividend', {}, (res) => { - checkSuccessResponse(res, false); + checkSuccessResponse(res); + checkDividends(res.data.dividends); done(); }); }); diff --git a/shared/consts/constModules/auctionConst.ts b/shared/consts/constModules/auctionConst.ts index e36d78459..1fd1f1e58 100644 --- a/shared/consts/constModules/auctionConst.ts +++ b/shared/consts/constModules/auctionConst.ts @@ -18,13 +18,13 @@ export const DIVIDEND_CODE_LEN = 8; // 世界拍卖开始时间需晚于军团拍卖结束时间 export const AUCTION_TIME = { - GUILD_BEGIN_HOUR: 8, // 军团拍卖开始 + GUILD_BEGIN_HOUR: 20, // 军团拍卖开始 GUILD_BEGIN_MIN: 20, // 军团拍卖开始 - WORLD_BEGIN_HOUR: 8, // 世界拍卖开始 + WORLD_BEGIN_HOUR: 20, // 世界拍卖开始 WORLD_BEGIN_MIN: 30, // 世界拍卖开始 - GUILD_END_HOUR: 8, // 军团拍卖结束 + GUILD_END_HOUR: 20, // 军团拍卖结束 GUILD_END_MIN: 30, // 军团拍卖结束 - WORLD_END_HOUR: 10, // 世界拍卖结束 + WORLD_END_HOUR: 22, // 世界拍卖结束 WORLD_END_MIN: 0, // 世界拍卖结束 }; @@ -35,4 +35,10 @@ export const DIVIDEND_STATUS = { SENT: 3, // 3:已发放 }; +export const LOT_STATUS = { + DEFAULT: 0, // 未开始 + ING: 1, // 竞拍中 + SOLD: 2, // 已拍出 +} + export const OFFER_RATIO = 1.1; diff --git a/shared/consts/dataName.ts b/shared/consts/dataName.ts index df78df153..cc774830b 100644 --- a/shared/consts/dataName.ts +++ b/shared/consts/dataName.ts @@ -9,4 +9,5 @@ export enum DATA_NAME { REFRESH_ACTIVE = 'GuildRefActive', WEEKLY_GUILD_SUM = 'WeeklyGuildSum', // 每周结算活跃和奖励 GAMEMAIL = 'Mail', + AUCTION_LOT = 'LotCode', } diff --git a/shared/db/Lot.ts b/shared/db/Lot.ts index 4b98ef808..dddd53ab5 100644 --- a/shared/db/Lot.ts +++ b/shared/db/Lot.ts @@ -34,10 +34,14 @@ export default class Lot extends BaseModel { maxPrice: number; // 一口价,最高价格 @prop({ required: true, type: BidRec, default: [] }) bidRoles: BidRec[]; + @prop({ required: true, type: String, default: [] }) + watchingRoles: string[]; @prop({ required: true }) begin: Date; // 竞拍开始时间 @prop({ required: true }) end: Date; // 竞拍结束时间 + @prop({ required: true, default: 0 }) + status: number; // 拍品状态,0:无人竞拍;1:竞拍中;2:已拍 public static async createRec(data: LotParam) { const code = genCode(8); @@ -56,13 +60,18 @@ export default class Lot extends BaseModel { return result; } - public static async findGuildLotsByBegin(guildCode: string, time: Date) { - const results = await LotModel.find({ guildCode, begin: { $eq: time } }).select('-_id -__v').lean(); + public static async findGuildLotsByBegin(guildCode: string, begin: Date) { + const results = await LotModel.find({ guildCode, begin }).select('-_id -__v').lean(); return results; } - public static async findWorldLotsByBegin(serverId: number, time: Date) { - const results = await LotModel.find({ serverId, begin: { $eq: time } }).select('-_id -__v').lean(); + public static async findWorldLotsByBegin(serverId: number, begin: Date) { + const results = await LotModel.find({ serverId, begin }).select('-_id -__v').lean(); + return results; + } + + public static async findWatchingLotsByBegin(roleId: string, serverId: number, begin: Date) { + const results = await LotModel.find({ serverId, begin, watchingRoles: roleId }, { new: true }).select('-_id -__v').lean(); return results; } @@ -71,6 +80,16 @@ export default class Lot extends BaseModel { const result: LotType = await LotModel.findOneAndUpdate({ code }, { ...data }, { new: true }).select('-_id -__v').lean(); return result; } + + public static async watchLot(code: string, roleId: string) { + const result: LotType = await LotModel.findOneAndUpdate({ code }, { $addToSet: { watchingRoles: roleId } }, { new: true }).select('-_id -__v').lean(); + return result; + } + + public static async unWatchLot(code: string, roleId: string) { + const result: LotType = await LotModel.findOneAndUpdate({ code }, { $pull: { watchingRoles: roleId } }, { new: true }).select('-_id -__v').lean(); + return result; + } } export const LotModel = getModelForClass(Lot); diff --git a/shared/pubUtils/timeUtil.ts b/shared/pubUtils/timeUtil.ts index 3428e2add..99dbe0d99 100644 --- a/shared/pubUtils/timeUtil.ts +++ b/shared/pubUtils/timeUtil.ts @@ -156,10 +156,10 @@ export function formatTime(d: Date, type: number) { * @param {number} [s=0] 给定秒 * @returns {Date} */ -export function getNextTime(date: Date, h: number, m = 0, s = 0): Date { +export function getNextTime(date: Date, h: number, m = 0, s = 0, ms = 0): Date { const milliseconds = date.getTime(); - const timeToday = new Date(milliseconds).setHours(h, m, s); - return timeToday > milliseconds ? new Date(timeToday) : new Date(timeToday + PER_DAY * PER_SECOND); + const timeToday = new Date(milliseconds).setHours(h, m, s, ms); + return timeToday < milliseconds ? new Date(timeToday) : new Date(timeToday + PER_DAY * PER_SECOND); } /**