2025-09-30

redis存储未读数据
This commit is contained in:
2025-09-30 13:41:41 +08:00
parent 46ce60e9b9
commit 51052e59a2
8 changed files with 331 additions and 21 deletions

193
package-lock.json generated
View File

@@ -9,7 +9,6 @@
"version": "0.0.1",
"license": "UNLICENSED",
"dependencies": {
"@nestjs/common": "^11.1.6",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
@@ -17,6 +16,7 @@
"@nestjs/typeorm": "^11.0.0",
"@nestjs/websockets": "^11.1.6",
"cors": "^2.8.5",
"date-fns": "^4.1.0",
"dotenv": "^17.2.2",
"minio": "^8.0.6",
"mysql2": "^3.15.0",
@@ -673,6 +673,7 @@
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.1.1.tgz",
"integrity": "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
@@ -1266,6 +1267,13 @@
}
}
},
"node_modules/@ioredis/commands": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz",
"integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==",
"optional": true,
"peer": true
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
@@ -2162,6 +2170,7 @@
"version": "11.1.6",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.6.tgz",
"integrity": "sha512-krKwLLcFmeuKDqngG2N/RuZHCs2ycsKcxWIDgcm7i1lf3sQ0iG03ci+DsP/r3FcT/eJDFsIHnKtNta2LIi7PzQ==",
"peer": true,
"dependencies": {
"file-type": "21.0.0",
"iterare": "1.2.1",
@@ -2530,6 +2539,71 @@
"url": "https://opencollective.com/pkgr"
}
},
"node_modules/@redis/bloom": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.8.2.tgz",
"integrity": "sha512-855DR0ChetZLarblio5eM0yLwxA9Dqq50t8StXKp5bAtLT0G+rZ+eRzzqxl37sPqQKjUudSYypz55o6nNhbz0A==",
"optional": true,
"peer": true,
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.8.2"
}
},
"node_modules/@redis/client": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-5.8.2.tgz",
"integrity": "sha512-WtMScno3+eBpTac1Uav2zugXEoXqaU23YznwvFgkPwBQVwEHTDgOG7uEAObtZ/Nyn8SmAMbqkEubJaMOvnqdsQ==",
"optional": true,
"peer": true,
"dependencies": {
"cluster-key-slot": "1.1.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@redis/json": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-5.8.2.tgz",
"integrity": "sha512-uxpVfas3I0LccBX9rIfDgJ0dBrUa3+0Gc8sEwmQQH0vHi7C1Rx1Qn8Nv1QWz5bohoeIXMICFZRcyDONvum2l/w==",
"optional": true,
"peer": true,
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.8.2"
}
},
"node_modules/@redis/search": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-5.8.2.tgz",
"integrity": "sha512-cNv7HlgayavCBXqPXgaS97DRPVWFznuzsAmmuemi2TMCx5scwLiP50TeZvUS06h/MG96YNPe6A0Zt57yayfxwA==",
"optional": true,
"peer": true,
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.8.2"
}
},
"node_modules/@redis/time-series": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.8.2.tgz",
"integrity": "sha512-g2NlHM07fK8H4k+613NBsk3y70R2JIM2dPMSkhIjl2Z17SYvaYKdusz85d7VYOrZBWtDrHV/WD2E3vGu+zni8A==",
"optional": true,
"peer": true,
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.8.2"
}
},
"node_modules/@sinclair/typebox": {
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
@@ -2568,6 +2642,7 @@
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz",
"integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==",
"peer": true,
"dependencies": {
"debug": "^4.4.0",
"fflate": "^0.8.2",
@@ -2584,7 +2659,8 @@
"node_modules/@tokenizer/token": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
"peer": true
},
"node_modules/@tsconfig/node10": {
"version": "1.0.11",
@@ -4416,6 +4492,16 @@
"node": ">=0.8"
}
},
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
"optional": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -4669,6 +4755,15 @@
"node": ">= 8"
}
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/dayjs": {
"version": "1.11.18",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
@@ -5508,7 +5603,8 @@
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"peer": true
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
@@ -5526,6 +5622,7 @@
"version": "21.0.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-21.0.0.tgz",
"integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==",
"peer": true,
"dependencies": {
"@tokenizer/inflate": "^0.2.7",
"strtok3": "^10.2.2",
@@ -6192,6 +6289,31 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ioredis": {
"version": "5.8.0",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.0.tgz",
"integrity": "sha512-AUXbKn9gvo9hHKvk6LbZJQSKn/qIfkWXrnsyL9Yrf+oeXmla9Nmf6XEumOddyhM8neynpK5oAV6r9r99KBuwzA==",
"optional": true,
"peer": true,
"dependencies": {
"@ioredis/commands": "1.4.0",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
"lodash.defaults": "^4.2.0",
"lodash.isarguments": "^3.1.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"engines": {
"node": ">=12.22.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ioredis"
}
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -7338,6 +7460,7 @@
"url": "https://buymeacoffee.com/borewit"
}
],
"peer": true,
"engines": {
"node": ">=13.2.0"
}
@@ -7371,6 +7494,20 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"optional": true,
"peer": true
},
"node_modules/lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
"optional": true,
"peer": true
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -8488,6 +8625,46 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/redis": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/redis/-/redis-5.8.2.tgz",
"integrity": "sha512-31vunZj07++Y1vcFGcnNWEf5jPoTkGARgfWI4+Tk55vdwHxhAvug8VEtW7Cx+/h47NuJTEg/JL77zAwC6E0OeA==",
"optional": true,
"peer": true,
"dependencies": {
"@redis/bloom": "5.8.2",
"@redis/client": "5.8.2",
"@redis/json": "5.8.2",
"@redis/search": "5.8.2",
"@redis/time-series": "5.8.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
"optional": true,
"peer": true,
"engines": {
"node": ">=4"
}
},
"node_modules/redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
"optional": true,
"peer": true,
"dependencies": {
"redis-errors": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/reflect-metadata": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
@@ -9098,6 +9275,13 @@
"node": ">=8"
}
},
"node_modules/standard-as-callback": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
"optional": true,
"peer": true
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -9321,6 +9505,7 @@
"version": "10.3.4",
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz",
"integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==",
"peer": true,
"dependencies": {
"@tokenizer/token": "^0.3.0"
},
@@ -9657,6 +9842,7 @@
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.1.tgz",
"integrity": "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==",
"peer": true,
"dependencies": {
"@borewit/text-codec": "^0.1.0",
"@tokenizer/token": "^0.3.0",
@@ -10207,6 +10393,7 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz",
"integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==",
"peer": true,
"engines": {
"node": ">=18"
},

View File

@@ -21,7 +21,6 @@
"generator:db": "typeorm-model-generator -h 192.168.0.4 -d test -p 3306 -u test -x n6M2XrssM2YpM5Mi -e mysql -o ./src/generator_entity"
},
"dependencies": {
"@nestjs/common": "^11.1.6",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
@@ -29,6 +28,7 @@
"@nestjs/typeorm": "^11.0.0",
"@nestjs/websockets": "^11.1.6",
"cors": "^2.8.5",
"date-fns": "^4.1.0",
"dotenv": "^17.2.2",
"minio": "^8.0.6",
"mysql2": "^3.15.0",

View File

@@ -12,6 +12,7 @@ export class UploadController {
@Post('file')
@UseInterceptors(FileInterceptor('file'))
async uploadFile(@UploadedFile() file: any) {
console.log(typeof file)
let path = "app_records"
return await this.fileService.uploadFile(
'jurongquan',
@@ -21,19 +22,4 @@ export class UploadController {
);
}
@Post('files')
@UseInterceptors(FileInterceptor('file'))
async uploadFiles(@UploadedFile() files: any) {
let path = "app_records"
for (let file in files) {
console.log(file)
// this.fileService.uploadFile(
// 'jurongquan',
// file.fileName,
// file.buffer,
// path,
// )
}
}
}

View File

@@ -1,4 +1,5 @@
import {Column, Entity, ManyToOne, PrimaryGeneratedColumn} from "typeorm";
import {format} from "date-fns";
@Entity("program", {schema: "test"})
export class ProgramEntity {
@@ -38,10 +39,22 @@ export class ProgramEntity {
name: "start_date",
nullable: true,
comment: "开始时间",
transformer: {
to: (value: Date) => value,
from: (value: string) => value ? format(value, "yyyy-MM-dd") : null
}
})
startDate: Date | null;
@Column("datetime", {name: "end_date", nullable: true, comment: "结束时间"})
@Column("datetime", {
name: "end_date",
nullable: true,
comment: "结束时间",
transformer: {
to: (value: Date) => value,
from: (value: string) => value ? format(value, "yyyy-MM-dd") : null
}
})
endDate: Date | null;
@Column("varchar", {

View File

@@ -10,6 +10,7 @@ import {Server, Socket} from 'socket.io';
import {ProgramGroupEntity, ProgramGroupMessageEntity, UsersEntity} from "../entity";
import {InjectRepository} from "@nestjs/typeorm";
import {Repository} from "typeorm";
import {RedisService} from "./redis.service";
@WebSocketGateway({
cors: {
@@ -22,6 +23,7 @@ export class ChatGateway implements OnGatewayInit {
@InjectRepository(ProgramGroupMessageEntity) private readonly messageRepository: Repository<ProgramGroupMessageEntity>,
@InjectRepository(ProgramGroupEntity) private readonly groupRepository: Repository<ProgramGroupEntity>,
@InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity>,
private readonly redisService: RedisService
) {
}
@@ -46,6 +48,7 @@ export class ChatGateway implements OnGatewayInit {
})
if (group != null) {
client.join(group.groupId.toString());
console.log(`客户端 ${client.id} 加入连接`)
}
}
@@ -55,6 +58,28 @@ export class ChatGateway implements OnGatewayInit {
*/
@SubscribeMessage('clientMsg')
async clientMessage(@MessageBody() data: ProgramGroupMessageEntity): Promise<any> {
// 不在线用户
let userIds = await this.activeUser(data.groupId)
if (userIds.length != 0) {
// 保存未读消息 redis
console.log(userIds)
for (let i = 0; i < userIds.length; i++) {
let item = userIds[i]
// 先取再存
let redisData = await this.redisService.getValue(`${data.groupId}_${item}`)
if (redisData == null) {
this.redisService.setValue(`${data.groupId}_${item}`, JSON.stringify([data]))
} else {
let arr: any = []
arr = JSON.parse(redisData)
arr.push(data)
this.redisService.setValue(`${data.groupId}_${item}`, JSON.stringify(arr))
}
}
}
// 保存记录
let saveMessage = await this.messageRepository.save(data)
@@ -67,4 +92,71 @@ export class ChatGateway implements OnGatewayInit {
this.server.to(data.groupId.toString()).emit('serverMsg', saveMessage)
}
async activeUser(roomId: any) {
// 获取房间应该存在的用户
let roomUsers = await this.groupRepository.findOne({
where: {
groupId: roomId
}
})
let userIdArr = Object.values((this.server.engine as any).clients).map(item => {
return (item as any).request._query.userId;
});
userIdArr = Array.from(new Set(userIdArr));
if (roomUsers != null) {
if (userIdArr.includes(roomUsers.userId.toString())) {
userIdArr.splice(userIdArr.indexOf(roomUsers.userId.toString()), 1)
} else {
userIdArr.push(roomUsers.userId.toString())
}
if (userIdArr.includes(roomUsers.chargeId.toString())) {
userIdArr.splice(userIdArr.indexOf(roomUsers.chargeId.toString()), 1)
} else {
userIdArr.push(roomUsers.chargeId.toString())
}
if (userIdArr.includes(roomUsers.customerId.toString())) {
userIdArr.splice(userIdArr.indexOf(roomUsers.customerId.toString()), 1)
} else {
userIdArr.push(roomUsers.customerId.toString())
}
}
return userIdArr;
}
/**
* 断开连接
* @param client
*/
@SubscribeMessage('leaveGroup')
async handleLeaveGroup(@ConnectedSocket() client: Socket, @MessageBody() groupEntity: ProgramGroupEntity): Promise<any> {
client.leave(groupEntity.groupId.toString())
client.disconnect(true)
console.log(`客户端 ${client.id} 断开连接`)
// let userIdArr = Object.values((this.server.engine as any).clients).map(item => {
// return (item as any).request._query.userId;
// });
// console.log(userIdArr)
}
// 手动清理所有连接
cleanupConnections() {
const rooms = this.server.sockets.adapter.rooms;
const sids = this.server.sockets.adapter.sids;
console.log('Current rooms:', rooms.size);
console.log('Current sids:', sids.size);
// 强制断开所有连接
this.server.sockets.sockets.forEach((socket) => {
socket.disconnect(true);
});
}
@SubscribeMessage('cleanup')
handleCleanup(@ConnectedSocket() client: Socket) {
this.cleanupConnections();
client.emit('cleanup_complete');
}
}

View File

@@ -1,3 +1,4 @@
export * from './programGroupService.service';
export * from './chat.gateway';
export * from './file.service';
export * from './redis.service'

View File

@@ -98,6 +98,12 @@ export class ProgramGroupService {
"program",
"group.program_id = program.id"
)
queryBuilder.leftJoinAndMapOne(
"group.user",
UsersEntity,
"user",
"group.user_id = user.id"
)
if (programGroup.customerId != null){
queryBuilder.andWhere("group.customer_id = :customerId", {customerId: programGroup.customerId})

View File

@@ -0,0 +1,25 @@
import {Injectable} from "@nestjs/common";
import Redis from 'ioredis';
@Injectable()
export class RedisService {
private readonly redisClient: Redis;
constructor() {
this.redisClient = new Redis({
host: '192.168.0.4',
port: 6379,
password: 'redis_P2bfmi',
username: 'default'
})
}
setValue(key: string, value: string){
return this.redisClient.set(key, value);
}
getValue(key: string) {
return this.redisClient.get(key);
}
}