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) {
// 获取算力账户信息
const contribution = await this.prisma.syncedSystemContribution.findUnique({
where: { accountType_regionCode: { accountType, regionCode } },
// 使用 findFirst 替代 findUnique因为 regionCode 可以为 null
const contribution = await this.prisma.syncedSystemContribution.findFirst({
where: {
accountType,
regionCode: regionCode === null ? { equals: null } : regionCode,
},
});
const whereClause = regionCode

View File

@ -540,20 +540,37 @@ export class CdcSyncService implements OnModuleInit {
private async handleSystemContributionUpdated(event: ServiceEvent, tx: TransactionClient): Promise<void> {
const { payload } = event;
await tx.syncedSystemContribution.upsert({
where: { accountType: payload.accountType },
create: {
accountType: payload.accountType,
name: payload.name,
contributionBalance: payload.contributionBalance || 0,
contributionNeverExpires: payload.contributionNeverExpires || false,
},
update: {
name: payload.name,
contributionBalance: payload.contributionBalance,
contributionNeverExpires: payload.contributionNeverExpires,
const accountType = payload.accountType;
const regionCode = payload.regionCode || null;
// 使用 findFirst + create/update 替代 upsert因为 regionCode 可以为 null
const existing = await tx.syncedSystemContribution.findFirst({
where: {
accountType,
regionCode: regionCode === null ? { equals: null } : regionCode,
},
});
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 regionCode = payload.regionCode || null;
// 使用 accountType + regionCode 作为复合唯一键
await tx.syncedSystemContribution.upsert({
// 使用 findFirst + create/update 替代 upsert因为 regionCode 可以为 null
const existing = await tx.syncedSystemContribution.findFirst({
where: {
accountType_regionCode: { accountType, regionCode },
},
create: {
accountType,
regionCode,
name: payload.name,
contributionBalance: payload.contributionBalance || 0,
contributionNeverExpires: true, // 系统账户算力永不过期
},
update: {
name: payload.name,
contributionBalance: payload.contributionBalance,
regionCode: regionCode === null ? { equals: null } : regionCode,
},
});
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;
// 先通过 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: {
accountType_regionCode: {
accountType,
regionCode: regionCode || null,
},
accountType,
regionCode: regionCodeValue === null ? { equals: null } : regionCodeValue,
},
});
@ -215,7 +215,7 @@ export class AdminController {
page: pageNum,
pageSize: pageSizeNum,
accountType,
regionCode: regionCode || null,
regionCode: regionCodeValue,
};
}
@ -247,7 +247,7 @@ export class AdminController {
page: pageNum,
pageSize: pageSizeNum,
accountType,
regionCode: regionCode || null,
regionCode: regionCodeValue,
};
}
@ -269,12 +269,12 @@ export class AdminController {
const skip = (pageNum - 1) * pageSizeNum;
// 先通过 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: {
accountType_regionCode: {
accountType,
regionCode: regionCode || null,
},
accountType,
regionCode: regionCodeValue === null ? { equals: null } : regionCodeValue,
},
});
@ -285,7 +285,7 @@ export class AdminController {
page: pageNum,
pageSize: pageSizeNum,
accountType,
regionCode: regionCode || null,
regionCode: regionCodeValue,
};
}
@ -305,7 +305,7 @@ export class AdminController {
transactions: transactions.map((tx) => ({
id: tx.id,
accountType,
regionCode: regionCode || null,
regionCode: regionCodeValue,
type: tx.type,
amount: tx.amount.toString(),
balanceBefore: tx.balanceBefore.toString(),
@ -319,7 +319,7 @@ export class AdminController {
page: pageNum,
pageSize: pageSizeNum,
accountType,
regionCode: regionCode || null,
regionCode: regionCodeValue,
};
}

View File

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

View File

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