fix(system-accounts): 修复 Prisma nullable regionCode 复合唯一键查询问题

- 将所有使用 accountType_regionCode 复合键的 findUnique 改为 findFirst
- 将所有 upsert 改为 findFirst + create/update 模式
- 原因:Prisma 复合唯一键不支持 nullable 字段的 findUnique 查询

影响的服务:
- mining-service: admin.controller.ts, system-mining-account.repository.ts
- mining-admin-service: cdc-sync.service.ts, system-accounts.service.ts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-20 21:45:13 -08:00
parent 40ac037c03
commit 7e61ac7ff2
5 changed files with 126 additions and 71 deletions

View File

@ -383,8 +383,12 @@ export class SystemAccountsService {
*/ */
async getSystemAccountContributionStats(accountType: string, regionCode: string | null) { async getSystemAccountContributionStats(accountType: string, regionCode: string | null) {
// 获取算力账户信息 // 获取算力账户信息
const contribution = await this.prisma.syncedSystemContribution.findUnique({ // 使用 findFirst 替代 findUnique因为 regionCode 可以为 null
where: { accountType_regionCode: { accountType, regionCode } }, const contribution = await this.prisma.syncedSystemContribution.findFirst({
where: {
accountType,
regionCode: regionCode === null ? { equals: null } : regionCode,
},
}); });
const whereClause = regionCode const whereClause = regionCode

View File

@ -540,20 +540,37 @@ export class CdcSyncService implements OnModuleInit {
private async handleSystemContributionUpdated(event: ServiceEvent, tx: TransactionClient): Promise<void> { private async handleSystemContributionUpdated(event: ServiceEvent, tx: TransactionClient): Promise<void> {
const { payload } = event; const { payload } = event;
await tx.syncedSystemContribution.upsert({ const accountType = payload.accountType;
where: { accountType: payload.accountType }, const regionCode = payload.regionCode || null;
create: {
accountType: payload.accountType, // 使用 findFirst + create/update 替代 upsert因为 regionCode 可以为 null
name: payload.name, const existing = await tx.syncedSystemContribution.findFirst({
contributionBalance: payload.contributionBalance || 0, where: {
contributionNeverExpires: payload.contributionNeverExpires || false, accountType,
}, regionCode: regionCode === null ? { equals: null } : regionCode,
update: {
name: payload.name,
contributionBalance: payload.contributionBalance,
contributionNeverExpires: payload.contributionNeverExpires,
}, },
}); });
if (existing) {
await tx.syncedSystemContribution.update({
where: { id: existing.id },
data: {
name: payload.name,
contributionBalance: payload.contributionBalance,
contributionNeverExpires: payload.contributionNeverExpires,
},
});
} else {
await tx.syncedSystemContribution.create({
data: {
accountType,
regionCode,
name: payload.name,
contributionBalance: payload.contributionBalance || 0,
contributionNeverExpires: payload.contributionNeverExpires || false,
},
});
}
} }
/** /**
@ -567,23 +584,33 @@ export class CdcSyncService implements OnModuleInit {
const accountType = payload.accountType; // OPERATION / PROVINCE / CITY / HEADQUARTERS const accountType = payload.accountType; // OPERATION / PROVINCE / CITY / HEADQUARTERS
const regionCode = payload.regionCode || null; const regionCode = payload.regionCode || null;
// 使用 accountType + regionCode 作为复合唯一键 // 使用 findFirst + create/update 替代 upsert因为 regionCode 可以为 null
await tx.syncedSystemContribution.upsert({ const existing = await tx.syncedSystemContribution.findFirst({
where: { where: {
accountType_regionCode: { accountType, regionCode },
},
create: {
accountType, accountType,
regionCode, regionCode: regionCode === null ? { equals: null } : regionCode,
name: payload.name,
contributionBalance: payload.contributionBalance || 0,
contributionNeverExpires: true, // 系统账户算力永不过期
},
update: {
name: payload.name,
contributionBalance: payload.contributionBalance,
}, },
}); });
if (existing) {
await tx.syncedSystemContribution.update({
where: { id: existing.id },
data: {
name: payload.name,
contributionBalance: payload.contributionBalance,
},
});
} else {
await tx.syncedSystemContribution.create({
data: {
accountType,
regionCode,
name: payload.name,
contributionBalance: payload.contributionBalance || 0,
contributionNeverExpires: true, // 系统账户算力永不过期
},
});
}
} }
/** /**

View File

@ -199,12 +199,12 @@ export class AdminController {
const skip = (pageNum - 1) * pageSizeNum; const skip = (pageNum - 1) * pageSizeNum;
// 先通过 accountType + regionCode 查找系统账户 // 先通过 accountType + regionCode 查找系统账户
const account = await this.prisma.systemMiningAccount.findUnique({ // 使用 findFirst 替代 findUnique因为 regionCode 可以为 null
const regionCodeValue = regionCode || null;
const account = await this.prisma.systemMiningAccount.findFirst({
where: { where: {
accountType_regionCode: { accountType,
accountType, regionCode: regionCodeValue === null ? { equals: null } : regionCodeValue,
regionCode: regionCode || null,
},
}, },
}); });
@ -215,7 +215,7 @@ export class AdminController {
page: pageNum, page: pageNum,
pageSize: pageSizeNum, pageSize: pageSizeNum,
accountType, accountType,
regionCode: regionCode || null, regionCode: regionCodeValue,
}; };
} }
@ -247,7 +247,7 @@ export class AdminController {
page: pageNum, page: pageNum,
pageSize: pageSizeNum, pageSize: pageSizeNum,
accountType, accountType,
regionCode: regionCode || null, regionCode: regionCodeValue,
}; };
} }
@ -269,12 +269,12 @@ export class AdminController {
const skip = (pageNum - 1) * pageSizeNum; const skip = (pageNum - 1) * pageSizeNum;
// 先通过 accountType + regionCode 查找系统账户 // 先通过 accountType + regionCode 查找系统账户
const account = await this.prisma.systemMiningAccount.findUnique({ // 使用 findFirst 替代 findUnique因为 regionCode 可以为 null
const regionCodeValue = regionCode || null;
const account = await this.prisma.systemMiningAccount.findFirst({
where: { where: {
accountType_regionCode: { accountType,
accountType, regionCode: regionCodeValue === null ? { equals: null } : regionCodeValue,
regionCode: regionCode || null,
},
}, },
}); });
@ -285,7 +285,7 @@ export class AdminController {
page: pageNum, page: pageNum,
pageSize: pageSizeNum, pageSize: pageSizeNum,
accountType, accountType,
regionCode: regionCode || null, regionCode: regionCodeValue,
}; };
} }
@ -305,7 +305,7 @@ export class AdminController {
transactions: transactions.map((tx) => ({ transactions: transactions.map((tx) => ({
id: tx.id, id: tx.id,
accountType, accountType,
regionCode: regionCode || null, regionCode: regionCodeValue,
type: tx.type, type: tx.type,
amount: tx.amount.toString(), amount: tx.amount.toString(),
balanceBefore: tx.balanceBefore.toString(), balanceBefore: tx.balanceBefore.toString(),
@ -319,7 +319,7 @@ export class AdminController {
page: pageNum, page: pageNum,
pageSize: pageSizeNum, pageSize: pageSizeNum,
accountType, accountType,
regionCode: regionCode || null, regionCode: regionCodeValue,
}; };
} }

View File

@ -531,7 +531,8 @@ export class MiningDistributionService {
let systemParticipantCount = 0; let systemParticipantCount = 0;
let pendingParticipantCount = 0; let pendingParticipantCount = 0;
const systemRedisData: Array<{ const systemRedisData: Array<{
accountType: string; // 改为字符串支持组合键 accountType: string;
regionCode: string | null;
reward: ShareAmount; reward: ShareAmount;
contribution: ShareAmount; contribution: ShareAmount;
}> = []; }> = [];

View File

@ -24,9 +24,11 @@ export class SystemMiningAccountRepository {
accountType: SystemAccountType, accountType: SystemAccountType,
regionCode: string | null, regionCode: string | null,
): Promise<SystemMiningAccountSnapshot | null> { ): Promise<SystemMiningAccountSnapshot | null> {
const record = await this.prisma.systemMiningAccount.findUnique({ // 使用 findFirst 替代 findUnique因为 regionCode 可以为 null
const record = await this.prisma.systemMiningAccount.findFirst({
where: { where: {
accountType_regionCode: { accountType, regionCode }, accountType,
regionCode: regionCode === null ? { equals: null } : regionCode,
}, },
}); });
@ -67,20 +69,26 @@ export class SystemMiningAccountRepository {
]; ];
for (const account of accounts) { for (const account of accounts) {
await this.prisma.systemMiningAccount.upsert({ // 使用 findFirst + create 替代 upsert因为 regionCode 可以为 null
const existing = await this.prisma.systemMiningAccount.findFirst({
where: { where: {
accountType_regionCode: { accountType: account.accountType, regionCode: null },
},
create: {
accountType: account.accountType, accountType: account.accountType,
regionCode: null, regionCode: { equals: null },
name: account.name,
totalMined: 0,
availableBalance: 0,
totalContribution: 0,
}, },
update: {},
}); });
if (!existing) {
await this.prisma.systemMiningAccount.create({
data: {
accountType: account.accountType,
regionCode: null,
name: account.name,
totalMined: 0,
availableBalance: 0,
totalContribution: 0,
},
});
}
} }
} }
@ -93,22 +101,33 @@ export class SystemMiningAccountRepository {
name: string, name: string,
contribution: ShareAmount, contribution: ShareAmount,
): Promise<void> { ): Promise<void> {
await this.prisma.systemMiningAccount.upsert({ // 使用 findFirst + create/update 替代 upsert因为 regionCode 可以为 null
const existing = await this.prisma.systemMiningAccount.findFirst({
where: { where: {
accountType_regionCode: { accountType, regionCode },
},
create: {
accountType, accountType,
regionCode, regionCode: regionCode === null ? { equals: null } : regionCode,
name,
totalContribution: contribution.value,
lastSyncedAt: new Date(),
},
update: {
totalContribution: contribution.value,
lastSyncedAt: new Date(),
}, },
}); });
if (existing) {
await this.prisma.systemMiningAccount.update({
where: { id: existing.id },
data: {
totalContribution: contribution.value,
lastSyncedAt: new Date(),
},
});
} else {
await this.prisma.systemMiningAccount.create({
data: {
accountType,
regionCode,
name,
totalContribution: contribution.value,
lastSyncedAt: new Date(),
},
});
}
} }
async getTotalContribution(): Promise<ShareAmount> { async getTotalContribution(): Promise<ShareAmount> {
@ -130,9 +149,11 @@ export class SystemMiningAccountRepository {
tx?: TransactionClient, tx?: TransactionClient,
): Promise<void> { ): Promise<void> {
const executeInTx = async (client: TransactionClient) => { const executeInTx = async (client: TransactionClient) => {
const account = await client.systemMiningAccount.findUnique({ // 使用 findFirst 替代 findUnique因为 regionCode 可以为 null
const account = await client.systemMiningAccount.findFirst({
where: { where: {
accountType_regionCode: { accountType, regionCode }, accountType,
regionCode: regionCode === null ? { equals: null } : regionCode,
}, },
}); });
@ -180,9 +201,11 @@ export class SystemMiningAccountRepository {
secondDistribution: ShareAmount, secondDistribution: ShareAmount,
minedAmount: ShareAmount, minedAmount: ShareAmount,
): Promise<void> { ): Promise<void> {
const account = await this.prisma.systemMiningAccount.findUnique({ // 使用 findFirst 替代 findUnique因为 regionCode 可以为 null
const account = await this.prisma.systemMiningAccount.findFirst({
where: { where: {
accountType_regionCode: { accountType, regionCode }, accountType,
regionCode: regionCode === null ? { equals: null } : regionCode,
}, },
}); });