feat(pending-actions): display accountSequence alongside userId

- Add accountSequence field to PendingActionResponseDto
- Add helper methods to fetch accountSequence from UserAccount
- Update queryActions and getAction to include accountSequence
- Update admin-web table and detail view to show both fields
- accountSequence displayed prominently, userId shown as secondary info

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-02 21:33:03 -08:00
parent 13dd42d2be
commit cbbef170e8
6 changed files with 90 additions and 8 deletions

View File

@ -557,7 +557,23 @@
"Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(android\\): add 5-minute polling timeout mechanism for keygen/sign\n\nImplements Electron''s checkAndTriggerKeygen\\(\\) polling fallback:\n- Adds polling every 2 seconds with 5-minute timeout\n- Triggers keygen/sign via synthetic session_started event on in_progress status\n- Handles gRPC stream disconnection when app goes to background\n- Shows timeout error in UI via existing error mechanism\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(go list:*)",
"Bash(adb install:*)",
"Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(tss\\): add real-time round progress from msg.Type\\(\\) parsing\n\nExtract current round number from tss-lib message type string using\nregex pattern `Round\\(\\\\d+\\)`. This enables real-time progress updates\n\\(1/4, 2/4... for keygen, 1/9, 2/9... for signing\\) instead of only\nshowing completion status.\n\nChanges across all three platforms:\n- tss-wasm/main.go: Add extractRoundFromMessageType\\(\\) and call\n OnProgress with parsed round on each outgoing message\n- service-party-android/tsslib/tsslib.go: Same implementation for\n Android gomobile binding\n- service-party-app/tss-party/main.go: Same implementation for\n Electron subprocess, with isKeygen parameter to distinguish\n keygen \\(4 rounds\\) vs signing \\(9 rounds\\)\n\nSafe fallback: Returns 0 if parsing fails, which doesn''t affect\nprotocol execution - only UI display.\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")"
"Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(tss\\): add real-time round progress from msg.Type\\(\\) parsing\n\nExtract current round number from tss-lib message type string using\nregex pattern `Round\\(\\\\d+\\)`. This enables real-time progress updates\n\\(1/4, 2/4... for keygen, 1/9, 2/9... for signing\\) instead of only\nshowing completion status.\n\nChanges across all three platforms:\n- tss-wasm/main.go: Add extractRoundFromMessageType\\(\\) and call\n OnProgress with parsed round on each outgoing message\n- service-party-android/tsslib/tsslib.go: Same implementation for\n Android gomobile binding\n- service-party-app/tss-party/main.go: Same implementation for\n Electron subprocess, with isKeygen parameter to distinguish\n keygen \\(4 rounds\\) vs signing \\(9 rounds\\)\n\nSafe fallback: Returns 0 if parsing fails, which doesn''t affect\nprotocol execution - only UI display.\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(node --input-type=module -e:*)",
"Bash(npx solc:*)",
"Bash(node /c/Users/dong/Desktop/rwadurian/contracts/deploy.mjs:*)",
"Bash(npm init:*)",
"Bash(node deploy.mjs:*)",
"Bash(npx solcjs@0.8.19:*)",
"Bash(node compile.mjs:*)",
"Bash(node verify-sig.mjs:*)",
"Bash(node deploy-ethers.mjs:*)",
"Bash(node transfer-all.mjs:*)",
"Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(android\\): add share export and import functionality\n\nAdd ability to backup wallet shares to files and restore from backups:\n\n- Add ShareBackup data class in Models.kt for backup format\n- Add exportShareBackup\\(\\) and importShareBackup\\(\\) in TssRepository\n- Add export/import state and methods in MainViewModel\n- Add file picker integration in MainActivity using ActivityResultContracts\n- Add import FAB button in WalletsScreen\n- Export saves as .tss-backup file with address and timestamp in filename\n- Import validates backup format and checks for duplicate wallets\n\nThe backup file contains all necessary data to restore a wallet share:\nsessionId, publicKey, encryptedShare, threshold, partyIndex, address.\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(dir /s /b \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\\\\backend\\\\services\\\\identity-service\\\\src\\\\api\\\\controllers\\\\*.ts\")",
"Bash(dir /s /b \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\\\\backend\\\\services\\\\identity-service\\\\src\\\\infrastructure\\\\persistence\\\\repositories\\\\*.ts\")",
"Bash(head:*)",
"Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(pending-actions\\): add user pending actions system\n\nAdd a fully optional pending actions system that allows admins to configure\nspecific tasks that users must complete after login.\n\nBackend \\(identity-service\\):\n- Add UserPendingAction model to Prisma schema\n- Add migration for user_pending_actions table\n- Add PendingActionService with full CRUD operations\n- Add user-facing API \\(GET list, POST complete\\)\n- Add admin API \\(CRUD, batch create\\)\n\nAdmin Web:\n- Add pending actions management page\n- Support single/batch create, edit, cancel, delete\n- View action details including completion time\n- Filter by userId, actionCode, status\n\nFlutter Mobile App:\n- Add PendingActionService and PendingActionCheckService\n- Add PendingActionsPage for forced task execution\n- Integrate into splash_page login flow\n- Users must complete all pending tasks in priority order\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(npm run type-check:*)"
],
"deny": [],
"ask": []

View File

@ -10,6 +10,9 @@ export class PendingActionResponseDto {
@ApiProperty({ example: '12345', description: '用户ID' })
userId: string;
@ApiPropertyOptional({ example: 'D25122700022', description: '用户账号序列号' })
accountSequence?: string;
@ApiProperty({ example: 'ADOPTION_WIZARD', description: '操作代码' })
actionCode: string;

View File

@ -224,8 +224,13 @@ export class PendingActionService {
this.prisma.userPendingAction.count({ where }),
]);
// 批量获取 accountSequence
const accountSequenceMap = await this.getAccountSequenceMap(items.map((item) => item.userId));
return {
items: items.map((action) => this.toResponseDto(action)),
items: items.map((action) =>
this.toResponseDto(action, accountSequenceMap.get(action.userId.toString())),
),
total,
page,
limit,
@ -244,7 +249,8 @@ export class PendingActionService {
throw new ApplicationError('操作不存在');
}
return this.toResponseDto(action);
const accountSequence = await this.getAccountSequence(action.userId);
return this.toResponseDto(action, accountSequence);
}
/**
@ -367,10 +373,11 @@ export class PendingActionService {
});
}
private toResponseDto(action: any): PendingActionResponseDto {
private toResponseDto(action: any, accountSequence?: string): PendingActionResponseDto {
return {
id: action.id.toString(),
userId: action.userId.toString(),
accountSequence,
actionCode: action.actionCode,
actionParams: action.actionParams as Record<string, any> | null,
status: action.status,
@ -381,4 +388,36 @@ export class PendingActionService {
createdBy: action.createdBy,
};
}
/**
* accountSequence
*/
private async getAccountSequenceMap(userIds: bigint[]): Promise<Map<string, string>> {
if (userIds.length === 0) return new Map();
const uniqueUserIds = [...new Set(userIds.map((id) => id.toString()))];
const users = await this.prisma.userAccount.findMany({
where: { userId: { in: uniqueUserIds.map((id) => BigInt(id)) } },
select: { userId: true, accountSequence: true },
});
const map = new Map<string, string>();
for (const user of users) {
if (user.accountSequence) {
map.set(user.userId.toString(), user.accountSequence);
}
}
return map;
}
/**
* accountSequence
*/
private async getAccountSequence(userId: bigint): Promise<string | undefined> {
const user = await this.prisma.userAccount.findUnique({
where: { userId },
select: { accountSequence: true },
});
return user?.accountSequence ?? undefined;
}
}

View File

@ -474,7 +474,7 @@ export default function PendingActionsPage() {
<thead>
<tr>
<th>ID</th>
<th>ID</th>
<th></th>
<th></th>
<th></th>
<th></th>
@ -489,7 +489,16 @@ export default function PendingActionsPage() {
return (
<tr key={action.id}>
<td>{action.id}</td>
<td>{action.userId}</td>
<td>
{action.accountSequence && (
<div className={styles.pendingActions__accountSequence}>
{action.accountSequence}
</div>
)}
<div className={styles.pendingActions__userId}>
ID: {action.userId}
</div>
</td>
<td>
<span className={styles.pendingActions__codeTag}>
{getActionCodeLabel(action.actionCode)}
@ -826,8 +835,11 @@ export default function PendingActionsPage() {
<span>{viewingAction.id}</span>
</div>
<div className={styles.pendingActions__detailRow}>
<span className={styles.pendingActions__detailLabel}>ID:</span>
<span>{viewingAction.userId}</span>
<span className={styles.pendingActions__detailLabel}>:</span>
<span>
{viewingAction.accountSequence && `${viewingAction.accountSequence} / `}
ID: {viewingAction.userId}
</span>
</div>
<div className={styles.pendingActions__detailRow}>
<span className={styles.pendingActions__detailLabel}>:</span>

View File

@ -136,6 +136,17 @@
font-weight: 500;
}
&__accountSequence {
font-weight: 600;
color: $primary-color;
font-size: 13px;
}
&__userId {
font-size: 12px;
color: $text-secondary;
}
&__actions {
display: flex;
gap: 8px;

View File

@ -24,6 +24,7 @@ export type ActionCode = (typeof ACTION_CODES)[keyof typeof ACTION_CODES] | stri
export interface PendingAction {
id: string;
userId: string;
accountSequence?: string; // 用户账号序列号 (如 D25122700022)
actionCode: string;
actionParams: Record<string, unknown> | null;
status: PendingActionStatus;