diff --git a/game-server/app/servers/gm/handler/gmHandler.ts b/game-server/app/servers/gm/handler/gmHandler.ts index 955548dcd..6571e041a 100644 --- a/game-server/app/servers/gm/handler/gmHandler.ts +++ b/game-server/app/servers/gm/handler/gmHandler.ts @@ -126,34 +126,14 @@ export class GmHandler { } } - async createMarquee(msg: { startTime: string, endTime: string, interval: number, content: string }, session: BackendSession) { + async createMarquee(msg: { startTime: number, endTime: number, interval: number, content: string }, session: BackendSession) { const { startTime, endTime, interval, content } = msg; - let marquee = await MarqueeModel.createData({ serverIds: [1], type: 2, startTime: new Date(startTime), endTime: new Date(endTime), interval, content }); + let marquee = await MarqueeModel.createData({ timeType: 2, startTime, endTime, interval, content }); return resResult(STATUS.SUCCESS, { code: marquee.code }); } - async sendMarquee(msg: { code: string }, session: BackendSession) { - const { code } = msg; - let systimerServers = this.app.getServersByType('systimer'); - for (let { id } of systimerServers) { - let result = await this.app.rpc.systimer.systimerRemote.setMarquee.toServer(id, code); - if (!result) return resResult(STATUS.GM_MARQUEE_ERR); - } - return resResult(STATUS.SUCCESS); - } - - async cancelMarquee(msg: { code: string }, session: BackendSession) { - const { code } = msg; - let systimerServers = this.app.getServersByType('systimer'); - for (let { id } of systimerServers) { - let result = await this.app.rpc.systimer.systimerRemote.cancelMarquee.toServer(id, code); - if (!result) return resResult(STATUS.GM_MARQUEE_CANCEL_ERR); - } - return resResult(STATUS.SUCCESS); - } - async updateActivity(msg: { activityId: number|string, groupId: number, beginTime: number, endTime: number, type: number, data: string }, session: BackendSession) { const { activityId, groupId, beginTime, endTime, type, data } = msg; const uid = session.get('uid'); diff --git a/game-server/app/servers/gm/handler/gmServerHandler.ts b/game-server/app/servers/gm/handler/gmServerHandler.ts index e595abe36..4cdfda50b 100644 --- a/game-server/app/servers/gm/handler/gmServerHandler.ts +++ b/game-server/app/servers/gm/handler/gmServerHandler.ts @@ -12,6 +12,7 @@ import { SendMailFun } from '../../../services/mailService'; import { createNewServer, sendOpenServerMail } from '../../../services/gmService'; import { initAutoCreateServer, initMaintenance, stopMaintenance } from '../../../services/timeTaskService'; import { isNumber } from 'util'; +import { MarqueeModel } from '../../../db/Marquee'; let timer: NodeJS.Timer; export default function (app: Application) { return new GmHandler(app); @@ -101,4 +102,23 @@ export class GmHandler { return resResult(STATUS.SUCCESS,); } + + async sendMarquee(msg: { code: string }, session: BackendSession) { + const { code } = msg; + let marquee = await MarqueeModel.findByCode(code); + if(!marquee) return resResult(STATUS.DB_DATA_NOT_FOUND); + + await MarqueeModel.updateData(code, { isEnable: true }); + let result = await this.app.rpc.systimer.systimerRemote.setMarquee.broadcast(marquee); + if (!result) return resResult(STATUS.GM_MARQUEE_ERR); + return resResult(STATUS.SUCCESS); + } + + async cancelMarquee(msg: { code: string }, session: BackendSession) { + const { code } = msg; + await MarqueeModel.updateData(code, { isEnable: false }); + let result = await this.app.rpc.systimer.systimerRemote.cancelMarquee.broadcast(code); + if (!result) return resResult(STATUS.GM_MARQUEE_CANCEL_ERR); + return resResult(STATUS.SUCCESS); + } } \ No newline at end of file diff --git a/game-server/app/servers/systimer/remote/systimerRemote.ts b/game-server/app/servers/systimer/remote/systimerRemote.ts index 9960f502b..05bef4807 100644 --- a/game-server/app/servers/systimer/remote/systimerRemote.ts +++ b/game-server/app/servers/systimer/remote/systimerRemote.ts @@ -7,6 +7,7 @@ import { setMarquee, cancelMarquee } from '../../../services/gmService'; import { setDicAuctionTime, setDicGuildActivity } from '../../../services/guildActivityService'; import { setWeek } from '../../../pubUtils/timeUtil'; import { ServerlistType } from '../../../db/Serverlist'; +import { MarqueeType } from '../../../db/Marquee'; export default function (app: Application) { return new SystimerRemote(app); @@ -51,8 +52,8 @@ export class SystimerRemote { * @description 设置跑马灯配置 * @param code 跑马灯唯一code */ - public async setMarquee(code: string) { - return await setMarquee(code); + public async setMarquee(marquee: MarqueeType) { + return await setMarquee(marquee); } public async cancelMarquee(code: string) { diff --git a/game-server/app/services/gmService.ts b/game-server/app/services/gmService.ts index 9d5defc00..5906d45f3 100644 --- a/game-server/app/services/gmService.ts +++ b/game-server/app/services/gmService.ts @@ -1,5 +1,5 @@ -import { MarqueeType, MarqueeModel } from "../db/Marquee"; -import { GM_MAIL_STATUS, GM_MAIL_TYPE, MAIL_TIME_TYPE, MARQUEE_TYPE, REF_CIRCLE_MAIL_TIME, SERVER_STATUS } from "../consts"; +import Marquee, { MarqueeType, MarqueeModel } from "../db/Marquee"; +import { GM_MAIL_STATUS, GM_MAIL_TYPE, MAIL_TIME_TYPE, MARQUEE_SHOW_TYPE, MARQUEE_TIME_TYPE, REF_CIRCLE_MAIL_TIME, SERVER_STATUS } from "../consts"; import { scheduleJob, scheduledJobs, Job } from 'node-schedule'; import { createMarqueeMsg as sysCreateMarqueeMsg, pushMarqueeMsg as sysPushMarqueeMsg } from './sysChatService'; import { GroupMessageType } from "../db/GroupMessage"; @@ -14,21 +14,21 @@ import { getSeconds, nowSeconds } from "../pubUtils/timeUtil"; import { CreateServerParam } from "../domain/backEndField/params"; import { RegionModel, RegionType } from "../db/Region"; import { GMMail as StategyMail } from "../db/ServerStategy"; +import { uniq } from "underscore"; // —————————————— 跑马灯 —————————————— // // 初始 export async function initMarquee() { - const marquees = await MarqueeModel.findEffectiveMarque(); + const marquees = await MarqueeModel.findEffectiveMarque(pinus.app.get('env')); for(let marquee of marquees) { await generateMarqueeSchedule(marquee); } } -export async function setMarquee(code: string) { - let marquee = await MarqueeModel.findByCode(code); - if(marquee.type == MARQUEE_TYPE.INSTANT) { - let msgDatas = await createMarqueeMsg(marquee); - await pushMarqueeMsg(msgDatas); +export async function setMarquee(marquee: MarqueeType) { + console.log('******', marquee.timeType, marquee) + if(marquee.timeType == MARQUEE_TIME_TYPE.INSTANT) { + await startMarquee(marquee); return true; } else { return await generateMarqueeSchedule(marquee); @@ -36,14 +36,14 @@ export async function setMarquee(code: string) { } async function generateMarqueeSchedule(marquee: MarqueeType) { + // 定时发送 let setSecondsMarquee = scheduledJobs[`setSeconds${marquee.code}`]; if(setSecondsMarquee) return false; // 已经在运行中了 - if(marquee.type != MARQUEE_TYPE.SCHEDULE) return false; - if(marquee.startTime.getTime() >= marquee.endTime.getTime()) return false; - if(marquee.endTime.getTime() < Date.now()) return false; - if(marquee.startTime.getTime() > Date.now()) { - let startJob = scheduleJob(`start${marquee.code}`, marquee.startTime.getTime(), async () => { + if(marquee.timeType != MARQUEE_TIME_TYPE.SCHEDULE) return false; + if(marquee.endTime && marquee.endTime < nowSeconds()) return false; + if(marquee.startTime > nowSeconds()) { + let startJob = scheduleJob(`start${marquee.code}`, marquee.startTime * 1000, async () => { await startMarquee(marquee, startJob) }); } else { @@ -54,37 +54,56 @@ async function generateMarqueeSchedule(marquee: MarqueeType) { async function startMarquee(marquee: MarqueeType, startJob?: Job) { console.log('************', `跑马灯定时器 ${marquee.code}开始`, '************'); - await MarqueeModel.updateData(marquee.code, { isRunning: true }); let msgDatas = await createMarqueeMsg(marquee); - await pushMarqueeMsg(msgDatas); - let secondsJob = scheduleJob(`setSeconds${marquee.code}`, `*/${marquee.interval} * * * * *`, async () => { - console.log(`*****setSeconds${marquee.code}****`) + if(marquee.showType == MARQUEE_SHOW_TYPE.ONCE) { // 一次性发送 await pushMarqueeMsg(msgDatas); - }); - let endJob = scheduleJob(`end${marquee.code}`, marquee.endTime.getTime(), async () => { if(startJob) { startJob.cancel(); startJob = undefined; } - if(secondsJob) { - secondsJob.cancel(); - secondsJob = undefined; - } - if(endJob) { - endJob.cancel(); - endJob = undefined; - } - await MarqueeModel.updateData(marquee.code, { isRunning: false }); - }); + } else { + await pushMarqueeMsg(msgDatas); + console.log('***', marquee.interval) + let secondsJob = scheduleJob(`setSeconds${marquee.code}`, `*/${marquee.interval} * * * * *`, async () => { + console.log(`*****setSeconds${marquee.code}****`) + await pushMarqueeMsg(msgDatas); + }); + let endJob = scheduleJob(`end${marquee.code}`, marquee.endTime * 1000, async () => { + if(startJob) { + startJob.cancel(); + startJob = undefined; + } + if(secondsJob) { + secondsJob.cancel(); + secondsJob = undefined; + } + if(endJob) { + endJob.cancel(); + endJob = undefined; + } + }); + } } async function createMarqueeMsg(marquee: MarqueeType) { - let { serverIds, content } = marquee; + let { receivers, content } = marquee; let msgDatas: GroupMessageType[] = []; - for(let serverId of serverIds) { + let serverIds: number[] = []; + for(let { env, serverId } of receivers) { + if(serverId == 0) { + let servers = await ServerlistModel.findByEnv(env); + for(let { serverId } of servers) { + serverIds.push(serverId); + } + } else { + serverIds.push(serverId); + } + } + for(let serverId of uniq(serverIds)) { let msgData = await sysCreateMarqueeMsg('', '系统', serverId, content); msgDatas.push(msgData); } + return msgDatas; } @@ -101,7 +120,6 @@ export async function cancelMarquee(code: string) { if(startMarquee) startMarquee.cancel(); if(setSecondsMarquee) setSecondsMarquee.cancel(); if(endMarquee) endMarquee.cancel(); - await MarqueeModel.updateData(code, { isRunning: false }); return true } diff --git a/game-server/app/services/timeTaskService.ts b/game-server/app/services/timeTaskService.ts index a6a32eecd..63a7a5cdd 100644 --- a/game-server/app/services/timeTaskService.ts +++ b/game-server/app/services/timeTaskService.ts @@ -552,7 +552,7 @@ function setStategyTimer(timerId: number, region: RegionType) { case SERVER_TIMER.TEN_HALF: cron = '0 30 10 * * *'; break; case SERVER_TIMER.FIFTEEN_HALF: - cron = '0 19 16 * * *'; break; + cron = '0 30 15 * * *'; break; case SERVER_TIMER.NINETEEN_HALF: cron = '0 30 19 * * *'; break; } diff --git a/gm-server/app/service/Game.ts b/gm-server/app/service/Game.ts index 4ee6662db..b0b0e0b54 100644 --- a/gm-server/app/service/Game.ts +++ b/gm-server/app/service/Game.ts @@ -8,13 +8,14 @@ import { DicRMB } from '@pubUtils/dictionary/DicRMB'; import { DicActivityType } from '@pubUtils/dictionary/DicActivityType'; import { DicTaskType } from '@pubUtils/dictionary/DicTaskType'; import { NoticeModel, NoticeTypeParam } from '@db/Notice'; -import { MarqueeModel, MarqueeParam } from '@db/Marquee'; +import Marquee, { MarqueeModel } from '@db/Marquee'; import { AccuseRecModel } from '@db/AccuseRec'; import { RegionModel } from '@db/Region'; import { ActivityGroupModel } from '@db/ActivityGroup'; import { nowSeconds } from '@pubUtils/timeUtil'; import { WhiteListModel } from '@db/RegionWhiteList'; import { RoleModel } from '@db/Role'; +import { SearchMarqueeParam } from '@domain/backEndField/search'; /** * Test Service @@ -305,21 +306,20 @@ export default class Game extends Service { } - public async getMarqueeList(page: number, pageSize: number, sortField: string, sortOrder: string, form: { type?: number, current?: boolean, content?: string }) { + public async getMarqueeList(page: number, pageSize: number, sortField: string, sortOrder: string, form: SearchMarqueeParam) { const { ctx } = this; const list = await MarqueeModel.findByCondition(page, pageSize, sortField, sortOrder, form); const total = await MarqueeModel.countByCondition( form ) return ctx.service.utils.resResult(STATUS.SUCCESS, { - list: list.map(cur => { - return { ...cur, startTime: cur.startTime?.getTime()||0, endTime: cur.endTime?.getTime()||0 } - }), total + list, total }); } - public async updateMarquee(code: string, params: MarqueeParam) { + public async updateMarquee(code: string, values: any) { const { ctx } = this; - if(params.startTime) params.startTime = new Date(params.startTime); - if(params.endTime) params.endTime = new Date(params.endTime); + let params = new Marquee(); + params.setByForm(values); + let result; if(code == 'new') { result = await MarqueeModel.createData(params); @@ -327,7 +327,7 @@ export default class Game extends Service { result = await MarqueeModel.updateData(code, params); } if(!result) return ctx.service.utils.resResult(STATUS.WRONG_PARMS); - return ctx.service.utils.resResult(STATUS.SUCCESS); + return ctx.service.utils.resResult(STATUS.SUCCESS, { code: result.code }); } public async getAccuse(page: number, pageSize: number, sortField: string, sortOrder: string, form: {}) { diff --git a/shared/consts/constModules/sysConst.ts b/shared/consts/constModules/sysConst.ts index 7eb6753a3..e7958ef35 100644 --- a/shared/consts/constModules/sysConst.ts +++ b/shared/consts/constModules/sysConst.ts @@ -697,7 +697,14 @@ export enum GACHA_CONTENT_TYPE { export const GACHA_OCCUPY_HID = 9999; // 抽卡里占位的武将 -export enum MARQUEE_TYPE { +// 跑马灯显示类型 +export enum MARQUEE_SHOW_TYPE { + ONCE = 1, // 一次性 + CIRCLE = 2, // 循环 +} + +// 推送时间类型 +export enum MARQUEE_TIME_TYPE { INSTANT = 1, // 发送后立刻推送 SCHEDULE = 2, // 定时器 } diff --git a/shared/consts/statusCode.ts b/shared/consts/statusCode.ts index c47e9a03c..580cf4682 100644 --- a/shared/consts/statusCode.ts +++ b/shared/consts/statusCode.ts @@ -244,6 +244,7 @@ export const STATUS = { ROLE_IS_NOT_INIT: { code: 30006, simStr: '玩家未初始化' }, AP_BUY_TIMES_LACK: { code: 30007, simStr: '购买次数不足' }, CONSUME_TYPE_ERR: { code: 30008, simStr: '道具类型错误' }, + DB_DATA_NOT_FOUND: { code: 30009, simStr: '数据库数据未找到' }, // 武将养成通用 30100 - 30199 diff --git a/shared/db/Marquee.ts b/shared/db/Marquee.ts index 97c5c33fd..0d545b152 100644 --- a/shared/db/Marquee.ts +++ b/shared/db/Marquee.ts @@ -1,36 +1,74 @@ import BaseModel from './BaseModel'; -import { index, getModelForClass, prop, DocumentType, modelOptions } from '@typegoose/typegoose'; -import { MARQUEE_TYPE } from '../consts'; +import { index, getModelForClass, prop, DocumentType, modelOptions, ReturnModelType, mongoose } from '@typegoose/typegoose'; +import { MARQUEE_SHOW_TYPE, MARQUEE_TIME_TYPE } from '../consts'; import { genCode } from '../pubUtils/util'; +import { SearchMarqueeParam } from '../domain/backEndField/search'; +import { nowSeconds } from '../pubUtils/timeUtil'; + /** * 跑马灯 **/ +class Receiver { + @prop({ required: true }) + regionId: number; + + @prop({ required: true }) + env: string; + + @prop({ required: true }) + serverId: number; +} + @modelOptions({ schemaOptions: { id: false } }) @index({ code: 1 }) export default class Marquee extends BaseModel { @prop({ required: true }) code: string; // 跑马灯唯一标识 - @prop({ required: true, type: Number }) - serverIds: number[]; // 推送服务器 - - @prop({ required: true, enum: MARQUEE_TYPE }) - type: MARQUEE_TYPE; // 推送类型 - @prop({ required: true }) - startTime: Date; // 活动开始时间 + receiverType: number; // 推送目标 - @prop({ required: true }) - endTime: Date; // 活动结束时间 + @prop({ required: true, type: Receiver, _id: false }) + receivers: Receiver[]; // 推送服务器 - @prop({ required: true }) - interval: number; // 活动结束时间 + @prop({ required: true, enum: MARQUEE_TIME_TYPE }) + timeType: MARQUEE_TIME_TYPE; // 推送时间类型,立刻/定时 + + @prop({ required: true, enum: MARQUEE_SHOW_TYPE }) + showType: MARQUEE_SHOW_TYPE; // 推送显示类型,一次性/循环 + + @prop({ required: true, default: 0 }) + startTime: number; // 循环开始时间/定时开始时间 + + @prop({ required: true, default: 0 }) + endTime: number; // 循环结束时间 + + @prop({ required: true, default: 0 }) + interval: number; // 推送间隔 @prop({ required: true }) content: string; // 广播内容 - @prop({ required: true }) - isRunning: boolean; // 当前正在发布 + @prop({ required: true, default: true }) + isEnable: boolean; // 当前正在发布 + + setByForm(obj: any) { + this.code = obj.code; + this.receiverType = obj.receiverType; + this.receivers = obj.receivers; + this.timeType = obj.timeType; + this.showType = obj.showType; + if(this.timeType == MARQUEE_TIME_TYPE.INSTANT) { + this.startTime = nowSeconds(); + } else { + this.startTime = obj.startTime; + } + if(this.showType == MARQUEE_SHOW_TYPE.CIRCLE) { + this.endTime = this.startTime + obj.continueMinute * 60; + this.interval = obj.interval; + } + this.content = obj.content; + } /** * 创建跑马灯 @@ -38,7 +76,7 @@ export default class Marquee extends BaseModel { * @param uid 后台操作人 */ public static async createData(params: MarqueeParam, uid = 1) { - const code = genCode(10); + const code = genCode(6); const doc = new MarqueeModel(); const update = Object.assign(doc.toJSON(), params, { code, createdBy: uid, updatedBy: uid }); const result: MarqueeType = await MarqueeModel.findOneAndUpdate({ code }, { $setOnInsert: update }, { new: true, upsert: true }).lean(); @@ -69,21 +107,33 @@ export default class Marquee extends BaseModel { /** * 查询所有生效中的跑马灯 */ - public static async findEffectiveMarque() { - const result: MarqueeType[] = await MarqueeModel.find({ type: MARQUEE_TYPE.SCHEDULE, startTime: { $lte: new Date() }, endTime: { $gte: new Date() } }).lean(); + public static async findEffectiveMarque(env: string) { + const result: MarqueeType[] = await MarqueeModel.find({ + $or: [ + { showType: MARQUEE_SHOW_TYPE.CIRCLE, startTime: { $lte: nowSeconds() }, endTime: { $gte: nowSeconds() }}, + { showType: MARQUEE_SHOW_TYPE.ONCE, startTime: { $lte: nowSeconds() }} + ], + isEnable: true, + 'receivers.env': env + }).lean(); return result; } - private static getSearchObj(form: { type?: number, current?: boolean, content?: string }) { + private static getSearchObj(form: SearchMarqueeParam) { let searchObj = {}; - if(form['type']) searchObj['type'] = form.type; - if(form['current']) searchObj['isRunning'] = form.current - if(form['content']) searchObj['content'] = { $regex: new RegExp(form.content.toString(), 'i') }; + if(form.code) searchObj['code'] = form.code; + if(form.createdBy) searchObj['createdBy'] = form.createdBy + if(form.createTimeStart && form.createTimeEnd) { + searchObj['createdAt'] = { $gt: new Date(form.createTimeStart * 1000), $lt: new Date(form.createTimeEnd * 1000) }; + } + if(form.sendTimeStart && form.sendTimeEnd) { + searchObj['startTime'] = { $gt: form.sendTimeStart, $lt: form.sendTimeEnd } + } return searchObj } - public static async findByCondition(page: number, pageSize: number, sortField: string = 'updatedAt', sortOrder: string = 'descend', form: { type?: number, current?: boolean, content?: string } = {}) { + public static async findByCondition(page: number, pageSize: number, sortField: string = 'updatedAt', sortOrder: string = 'descend', form: SearchMarqueeParam = {}) { let searchObj = this.getSearchObj(form); let sort = {}; @@ -94,12 +144,12 @@ export default class Marquee extends BaseModel { sort[sortField] = -1; } } - const result: MarqueeType[] = await MarqueeModel.find(searchObj).limit(pageSize).skip((page - 1) * pageSize).sort(sort).select('+sort +showStartTime +showEndTime +serverType +isEnable').lean({ getters: true, virtuals: true }); + const result: MarqueeType[] = await MarqueeModel.find(searchObj).limit(pageSize).skip((page - 1) * pageSize).sort(sort).lean({ getters: true, virtuals: true }); return result; } - public static async countByCondition(form: { type?: number, current?: boolean, content?: string } = {}) { + public static async countByCondition(form: SearchMarqueeParam = {}) { let searchObj = this.getSearchObj(form); const result = await MarqueeModel.count(searchObj); @@ -108,7 +158,12 @@ export default class Marquee extends BaseModel { } -export const MarqueeModel = getModelForClass(Marquee); +export let MarqueeModel: ReturnModelType; +export function loadMarqueeModel(connect: mongoose.Connection) { + MarqueeModel = getModelForClass(Marquee, { + existingConnection: connect + }); +} export interface MarqueeType extends Pick, keyof Marquee> {}; export type MarqueeParam = Partial; diff --git a/shared/db/index.ts b/shared/db/index.ts index 42adb0b1e..0cfaef575 100644 --- a/shared/db/index.ts +++ b/shared/db/index.ts @@ -8,6 +8,7 @@ import { loadRegionModel } from "./Region"; import { loadServerlistModel } from "./Serverlist"; import { loadGMMailModel } from './GMMail'; import { loadCounterModal } from "./CounterAll"; +import { loadMarqueeModel } from "./Marquee"; export function loadGmDb(connect: mongoose.Connection) { // console.log('************') @@ -20,4 +21,5 @@ export function loadGmDb(connect: mongoose.Connection) { loadServerlistModel(connect); loadGMMailModel(connect); loadCounterModal(connect); + loadMarqueeModel(connect); } \ No newline at end of file diff --git a/shared/domain/backEndField/search.ts b/shared/domain/backEndField/search.ts index 2cc308724..9308474de 100644 --- a/shared/domain/backEndField/search.ts +++ b/shared/domain/backEndField/search.ts @@ -34,4 +34,13 @@ export interface SearchMailParam { status?: GM_MAIL_STATUS; mailType?: GM_MAIL_TYPE; hasGoods?: boolean; +} + +export interface SearchMarqueeParam { + code?: string; + createdBy?: number; + createTimeStart?: number; + createTimeEnd?: number; + sendTimeStart?: number; + sendTimeEnd?: number; } \ No newline at end of file diff --git a/web-server/app/middleware/getIp.ts b/web-server/app/middleware/getIp.ts index 657ef2856..0c764ab73 100644 --- a/web-server/app/middleware/getIp.ts +++ b/web-server/app/middleware/getIp.ts @@ -3,7 +3,7 @@ import { Context } from 'egg'; module.exports = () => { return async function parmsDecode(ctx: Context, next) { - let xRealIp = typeof ctx.header['x-real-ip'] == 'string'? ctx.header['x-real-ip']: ctx.header['x-real-ip'][0]; + let xRealIp = ctx.header['x-real-ip'] && (typeof ctx.header['x-real-ip'] == 'string'? ctx.header['x-real-ip']: ctx.header['x-real-ip'][0]); ctx.clientIp = xRealIp||ctx.request.ip; console.log('*****', ctx.clientIp); await next();