refactor(identity): remove province/city/address fields

- Remove provinceCode, cityCode, address from UserAccount aggregate
- Remove ProvinceCode, CityCode value objects
- Remove UserLocationUpdatedEvent domain event
- Update Prisma schema to drop province/city/address columns
- Update repository, mapper, handlers, services and DTOs
- Clean up tests and factory files

Province/city should belong to adoption-service as transaction data,
not identity-service user data.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-07 11:23:26 -08:00
parent fbec0b9112
commit 2705812826
19 changed files with 28 additions and 154 deletions

View File

@ -30,7 +30,13 @@
"Bash(cd:*)",
"Bash(curl:*)",
"Bash(git revert:*)",
"Bash(del \"c:\\Users\\dong\\Desktop\\rwadurian\\backend\\services\\identity-service\\src\\infrastructure\\persistence\\entities\\user-device.entity.ts\")"
"Bash(del \"c:\\Users\\dong\\Desktop\\rwadurian\\backend\\services\\identity-service\\src\\infrastructure\\persistence\\entities\\user-device.entity.ts\")",
"Bash(cmd /c \"cd /d c:\\Users\\dong\\Desktop\\rwadurian\\backend\\services\\identity-service && npm run build\")",
"Bash(cmd /c \"cd /d c:\\Users\\dong\\Desktop\\rwadurian\\backend\\services\\identity-service && npx nest build\")",
"Bash(cmd /c \"cd /d c:\\Users\\dong\\Desktop\\rwadurian && git add -A && git status\")",
"Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" status)",
"Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" add -A)",
"Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" commit -m \"$(cat <<''EOF''\nrefactor(identity): remove province/city/address fields\n\n- Remove provinceCode, cityCode, address from UserAccount aggregate\n- Remove ProvinceCode, CityCode value objects\n- Remove UserLocationUpdatedEvent domain event\n- Update Prisma schema to drop province/city/address columns\n- Update repository, mapper, handlers, services and DTOs\n- Clean up tests and factory files\n\nProvince/city should belong to adoption-service as transaction data,\nnot identity-service user data.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>\nEOF\n)\")"
],
"deny": [],
"ask": []

View File

@ -10,39 +10,34 @@ datasource db {
model UserAccount {
userId BigInt @id @default(autoincrement()) @map("user_id")
accountSequence BigInt @unique @map("account_sequence")
phoneNumber String? @unique @map("phone_number") @db.VarChar(20)
nickname String @db.VarChar(100)
avatarUrl String? @map("avatar_url") @db.Text
inviterSequence BigInt? @map("inviter_sequence")
referralCode String @unique @map("referral_code") @db.VarChar(10)
provinceCode String @map("province_code") @db.VarChar(10)
cityCode String @map("city_code") @db.VarChar(10)
address String? @db.VarChar(500)
kycStatus String @default("NOT_VERIFIED") @map("kyc_status") @db.VarChar(20)
realName String? @map("real_name") @db.VarChar(100)
idCardNumber String? @map("id_card_number") @db.VarChar(20)
idCardFrontUrl String? @map("id_card_front_url") @db.VarChar(500)
idCardBackUrl String? @map("id_card_back_url") @db.VarChar(500)
kycVerifiedAt DateTime? @map("kyc_verified_at")
status String @default("ACTIVE") @db.VarChar(20)
registeredAt DateTime @default(now()) @map("registered_at")
lastLoginAt DateTime? @map("last_login_at")
updatedAt DateTime @updatedAt @map("updated_at")
devices UserDevice[]
walletAddresses WalletAddress[]
@@index([phoneNumber], name: "idx_phone")
@@index([accountSequence], name: "idx_sequence")
@@index([referralCode], name: "idx_referral_code")
@@index([inviterSequence], name: "idx_inviter")
@@index([provinceCode, cityCode], name: "idx_province_city")
@@index([kycStatus], name: "idx_kyc_status")
@@index([status], name: "idx_status")
@@map("user_accounts")

View File

@ -86,7 +86,7 @@ export class UserAccountController {
return this.userService.register(
new RegisterCommand(
dto.phoneNumber, dto.smsCode, dto.deviceId,
dto.provinceCode, dto.cityCode, dto.deviceName, dto.inviterReferralCode,
dto.deviceName, dto.inviterReferralCode,
),
);
}

View File

@ -47,16 +47,6 @@ export class RegisterDto {
@IsNotEmpty()
deviceId: string;
@ApiProperty()
@IsString()
@IsNotEmpty()
provinceCode: string;
@ApiProperty()
@IsString()
@IsNotEmpty()
cityCode: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()

View File

@ -35,15 +35,6 @@ export class UserProfileDto {
@ApiProperty()
referralCode: string;
@ApiProperty()
province: string;
@ApiProperty()
city: string;
@ApiProperty({ nullable: true })
address: string | null;
@ApiProperty({ type: [WalletAddressDto] })
walletAddresses: WalletAddressDto[];

View File

@ -3,7 +3,7 @@ import { AutoCreateAccountCommand } from './auto-create-account.command';
import { UserAccountRepository, USER_ACCOUNT_REPOSITORY } from '@/domain/repositories/user-account.repository.interface';
import { UserAccount } from '@/domain/aggregates/user-account/user-account.aggregate';
import { AccountSequenceGeneratorService, UserValidatorService } from '@/domain/services';
import { ReferralCode, AccountSequence, ProvinceCode, CityCode } from '@/domain/value-objects';
import { ReferralCode, AccountSequence } from '@/domain/value-objects';
import { TokenService } from '@/application/services/token.service';
import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service';
import { ApplicationError } from '@/shared/exceptions/domain.exception';
@ -63,8 +63,6 @@ export class AutoCreateAccountHandler {
deviceName: deviceNameStr,
deviceInfo: command.deviceName, // 100% 保持原样存储
inviterSequence,
province: ProvinceCode.create('DEFAULT'),
city: CityCode.create('DEFAULT'),
nickname: identity.username,
avatarSvg: identity.avatarSvg,
});

View File

@ -47,8 +47,6 @@ export class RegisterCommand {
public readonly phoneNumber: string,
public readonly smsCode: string,
public readonly deviceId: string,
public readonly provinceCode: string,
public readonly cityCode: string,
public readonly deviceName?: string,
public readonly inviterReferralCode?: string,
) {}
@ -75,7 +73,6 @@ export class UpdateProfileCommand {
public readonly userId: string,
public readonly nickname?: string,
public readonly avatarUrl?: string,
public readonly address?: string,
) {}
}
@ -219,9 +216,6 @@ export interface UserProfileDTO {
nickname: string;
avatarUrl: string | null;
referralCode: string;
province: string;
city: string;
address: string | null;
walletAddresses: Array<{ chainType: string; address: string }>;
kycStatus: string;
kycInfo: { realName: string; idCardNumber: string } | null;

View File

@ -27,9 +27,6 @@ export class GetMyProfileHandler {
nickname: account.nickname,
avatarUrl: account.avatarUrl,
referralCode: account.referralCode.value,
province: account.province.value,
city: account.city.value,
address: account.addressDetail,
walletAddresses: account.getAllWalletAddresses().map((wa) => ({
chainType: wa.chainType,
address: wa.address,

View File

@ -3,7 +3,7 @@ import { UserApplicationService } from './user-application.service';
import { USER_ACCOUNT_REPOSITORY, UserAccountRepository, ReferralLinkData, CreateReferralLinkParams } from '@/domain/repositories/user-account.repository.interface';
import { MPC_KEY_SHARE_REPOSITORY, MpcKeyShareRepository } from '@/domain/repositories/mpc-key-share.repository.interface';
import { UserAccount } from '@/domain/aggregates/user-account/user-account.aggregate';
import { AccountSequence, ReferralCode, UserId, ProvinceCode, CityCode, AccountStatus, KYCStatus, DeviceInfo } from '@/domain/value-objects';
import { AccountSequence, ReferralCode, UserId, AccountStatus, KYCStatus, DeviceInfo } from '@/domain/value-objects';
import { ConfigService } from '@nestjs/config';
import { ValidateReferralCodeQuery, GetReferralStatsQuery, GenerateReferralLinkCommand } from '@/application/commands';
import { ApplicationError } from '@/shared/exceptions/domain.exception';
@ -42,9 +42,6 @@ describe('UserApplicationService - Referral APIs', () => {
avatarUrl: params.avatarUrl ?? null,
inviterSequence: params.inviterSequence ?? null,
referralCode: params.referralCode || 'ABC123',
province: '110000',
city: '110100',
address: null,
walletAddresses: [],
kycInfo: null,
kycStatus: KYCStatus.NOT_VERIFIED,

View File

@ -7,7 +7,7 @@ import {
AccountSequenceGeneratorService, UserValidatorService,
} from '@/domain/services';
import {
UserId, PhoneNumber, ReferralCode, AccountSequence, ProvinceCode, CityCode,
UserId, PhoneNumber, ReferralCode, AccountSequence,
ChainType, KYCInfo,
} from '@/domain/value-objects';
import { TokenService } from './token.service';
@ -106,8 +106,6 @@ export class UserApplicationService {
deviceName: deviceNameStr,
deviceInfo: command.deviceName, // 100% 保持原样存储
inviterSequence,
province: ProvinceCode.create('DEFAULT'),
city: CityCode.create('DEFAULT'),
nickname: identity.username,
avatarSvg: identity.avatarSvg,
});
@ -298,8 +296,6 @@ export class UserApplicationService {
initialDeviceId: command.deviceId,
deviceName: command.deviceName,
inviterSequence,
province: ProvinceCode.create(command.provinceCode),
city: CityCode.create(command.cityCode),
});
await this.userRepository.save(account);
@ -466,9 +462,6 @@ export class UserApplicationService {
nickname: account.nickname,
avatarUrl: account.avatarUrl,
referralCode: account.referralCode.value,
province: account.province.value,
city: account.city.value,
address: account.addressDetail,
walletAddresses: account.getAllWalletAddresses().map((wa) => ({
chainType: wa.chainType,
address: wa.address,

View File

@ -1,6 +1,6 @@
import { DomainError } from '@/shared/exceptions/domain.exception';
import {
UserId, AccountSequence, PhoneNumber, ReferralCode, ProvinceCode, CityCode,
UserId, AccountSequence, PhoneNumber, ReferralCode,
DeviceInfo, ChainType, KYCInfo, KYCStatus, AccountStatus,
} from '@/domain/value-objects';
import { WalletAddress } from '@/domain/entities/wallet-address.entity';
@ -9,7 +9,7 @@ import {
DeviceAddedEvent, DeviceRemovedEvent, PhoneNumberBoundEvent,
WalletAddressBoundEvent, MultipleWalletAddressesBoundEvent,
KYCSubmittedEvent, KYCVerifiedEvent, KYCRejectedEvent,
UserLocationUpdatedEvent, UserAccountFrozenEvent, UserAccountDeactivatedEvent,
UserAccountFrozenEvent, UserAccountDeactivatedEvent,
} from '@/domain/events';
export class UserAccount {
@ -21,9 +21,6 @@ export class UserAccount {
private _avatarUrl: string | null;
private readonly _inviterSequence: AccountSequence | null;
private readonly _referralCode: ReferralCode;
private _province: ProvinceCode;
private _city: CityCode;
private _address: string | null;
private _walletAddresses: Map<ChainType, WalletAddress>;
private _kycInfo: KYCInfo | null;
private _kycStatus: KYCStatus;
@ -41,9 +38,6 @@ export class UserAccount {
get avatarUrl(): string | null { return this._avatarUrl; }
get inviterSequence(): AccountSequence | null { return this._inviterSequence; }
get referralCode(): ReferralCode { return this._referralCode; }
get province(): ProvinceCode { return this._province; }
get city(): CityCode { return this._city; }
get addressDetail(): string | null { return this._address; }
get kycInfo(): KYCInfo | null { return this._kycInfo; }
get kycStatus(): KYCStatus { return this._kycStatus; }
get status(): AccountStatus { return this._status; }
@ -58,7 +52,6 @@ export class UserAccount {
userId: UserId, accountSequence: AccountSequence, devices: Map<string, DeviceInfo>,
phoneNumber: PhoneNumber | null, nickname: string, avatarUrl: string | null,
inviterSequence: AccountSequence | null, referralCode: ReferralCode,
province: ProvinceCode, city: CityCode, address: string | null,
walletAddresses: Map<ChainType, WalletAddress>, kycInfo: KYCInfo | null,
kycStatus: KYCStatus, status: AccountStatus, registeredAt: Date,
lastLoginAt: Date | null, updatedAt: Date,
@ -71,9 +64,6 @@ export class UserAccount {
this._avatarUrl = avatarUrl;
this._inviterSequence = inviterSequence;
this._referralCode = referralCode;
this._province = province;
this._city = city;
this._address = address;
this._walletAddresses = walletAddresses;
this._kycInfo = kycInfo;
this._kycStatus = kycStatus;
@ -89,8 +79,6 @@ export class UserAccount {
deviceName?: string;
deviceInfo?: Record<string, unknown>; // 完整的设备信息 JSON
inviterSequence: AccountSequence | null;
province: ProvinceCode;
city: CityCode;
nickname?: string;
avatarSvg?: string;
}): UserAccount {
@ -107,7 +95,7 @@ export class UserAccount {
const account = new UserAccount(
UserId.create(0), params.accountSequence, devices, null,
nickname, avatarUrl, params.inviterSequence,
ReferralCode.generate(), params.province, params.city, null,
ReferralCode.generate(),
new Map(), null, KYCStatus.NOT_VERIFIED, AccountStatus.ACTIVE,
new Date(), null, new Date(),
);
@ -117,8 +105,6 @@ export class UserAccount {
accountSequence: params.accountSequence.value,
initialDeviceId: params.initialDeviceId,
inviterSequence: params.inviterSequence?.value || null,
province: params.province.value,
city: params.city.value,
registeredAt: account._registeredAt,
}));
@ -132,8 +118,6 @@ export class UserAccount {
deviceName?: string;
deviceInfo?: Record<string, unknown>; // 完整的设备信息 JSON
inviterSequence: AccountSequence | null;
province: ProvinceCode;
city: CityCode;
}): UserAccount {
const devices = new Map<string, DeviceInfo>();
devices.set(params.initialDeviceId, new DeviceInfo(
@ -145,7 +129,7 @@ export class UserAccount {
const account = new UserAccount(
UserId.create(0), params.accountSequence, devices, params.phoneNumber,
`用户${params.accountSequence.value}`, null, params.inviterSequence,
ReferralCode.generate(), params.province, params.city, null,
ReferralCode.generate(),
new Map(), null, KYCStatus.NOT_VERIFIED, AccountStatus.ACTIVE,
new Date(), null, new Date(),
);
@ -156,8 +140,6 @@ export class UserAccount {
phoneNumber: params.phoneNumber.value,
initialDeviceId: params.initialDeviceId,
inviterSequence: params.inviterSequence?.value || null,
province: params.province.value,
city: params.city.value,
registeredAt: account._registeredAt,
}));
@ -168,7 +150,6 @@ export class UserAccount {
userId: string; accountSequence: number; devices: DeviceInfo[];
phoneNumber: string | null; nickname: string; avatarUrl: string | null;
inviterSequence: number | null; referralCode: string;
province: string; city: string; address: string | null;
walletAddresses: WalletAddress[]; kycInfo: KYCInfo | null;
kycStatus: KYCStatus; status: AccountStatus;
registeredAt: Date; lastLoginAt: Date | null; updatedAt: Date;
@ -188,9 +169,6 @@ export class UserAccount {
params.avatarUrl,
params.inviterSequence ? AccountSequence.create(params.inviterSequence) : null,
ReferralCode.create(params.referralCode),
ProvinceCode.create(params.province),
CityCode.create(params.city),
params.address,
walletMap,
params.kycInfo,
params.kycStatus,
@ -243,24 +221,13 @@ export class UserAccount {
return Array.from(this._devices.values());
}
updateProfile(params: { nickname?: string; avatarUrl?: string; address?: string }): void {
updateProfile(params: { nickname?: string; avatarUrl?: string }): void {
this.ensureActive();
if (params.nickname) this._nickname = params.nickname;
if (params.avatarUrl !== undefined) this._avatarUrl = params.avatarUrl;
if (params.address !== undefined) this._address = params.address;
this._updatedAt = new Date();
}
updateLocation(province: ProvinceCode, city: CityCode): void {
this.ensureActive();
this._province = province;
this._city = city;
this._updatedAt = new Date();
this.addDomainEvent(new UserLocationUpdatedEvent({
userId: this.userId.toString(), province: province.value, city: city.value,
}));
}
bindPhoneNumber(phoneNumber: PhoneNumber): void {
this.ensureActive();
if (this._phoneNumber) throw new DomainError('已绑定手机号,不可重复绑定');

View File

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { UserAccount } from './user-account.aggregate';
import { AccountSequence, PhoneNumber, ProvinceCode, CityCode } from '@/domain/value-objects';
import { AccountSequence, PhoneNumber } from '@/domain/value-objects';
@Injectable()
export class UserAccountFactory {
@ -9,8 +9,6 @@ export class UserAccountFactory {
initialDeviceId: string;
deviceName?: string;
inviterSequence: AccountSequence | null;
province: ProvinceCode;
city: CityCode;
}): UserAccount {
return UserAccount.createAutomatic(params);
}
@ -21,8 +19,6 @@ export class UserAccountFactory {
initialDeviceId: string;
deviceName?: string;
inviterSequence: AccountSequence | null;
province: ProvinceCode;
city: CityCode;
}): UserAccount {
return UserAccount.create(params);
}

View File

@ -1,5 +1,5 @@
import { UserAccount } from './user-account.aggregate';
import { AccountSequence, ProvinceCode, CityCode } from '@/domain/value-objects';
import { AccountSequence } from '@/domain/value-objects';
import { DomainError } from '@/shared/exceptions/domain.exception';
describe('UserAccount', () => {
@ -9,8 +9,6 @@ describe('UserAccount', () => {
initialDeviceId: 'device-001',
deviceName: 'Test Device',
inviterSequence: null,
province: ProvinceCode.create('110000'),
city: CityCode.create('110100'),
});
};
@ -43,7 +41,7 @@ describe('UserAccount', () => {
account.addDevice('device-003');
account.addDevice('device-004');
account.addDevice('device-005');
expect(() => account.addDevice('device-006')).toThrow(DomainError);
});
});

View File

@ -17,8 +17,6 @@ export class UserAccountAutoCreatedEvent extends DomainEvent {
accountSequence: number;
initialDeviceId: string;
inviterSequence: number | null;
province: string;
city: string;
registeredAt: Date;
},
) {
@ -38,8 +36,6 @@ export class UserAccountCreatedEvent extends DomainEvent {
phoneNumber: string;
initialDeviceId: string;
inviterSequence: number | null;
province: string;
city: string;
registeredAt: Date;
},
) {
@ -143,16 +139,6 @@ export class KYCRejectedEvent extends DomainEvent {
}
}
export class UserLocationUpdatedEvent extends DomainEvent {
constructor(public readonly payload: { userId: string; province: string; city: string }) {
super();
}
get eventType(): string {
return 'UserLocationUpdated';
}
}
export class UserAccountFrozenEvent extends DomainEvent {
constructor(public readonly payload: { userId: string; reason: string }) {
super();

View File

@ -39,7 +39,7 @@ export interface UserAccountRepository {
getMaxAccountSequence(): Promise<AccountSequence | null>;
getNextAccountSequence(): Promise<AccountSequence>;
findUsers(
filters?: { status?: AccountStatus; kycStatus?: KYCStatus; province?: string; city?: string; keyword?: string },
filters?: { status?: AccountStatus; kycStatus?: KYCStatus; keyword?: string },
pagination?: Pagination,
): Promise<UserAccount[]>;
countUsers(filters?: { status?: AccountStatus; kycStatus?: KYCStatus }): Promise<number>;

View File

@ -97,23 +97,6 @@ export class ReferralCode {
}
}
// ============ ProvinceCode & CityCode ============
export class ProvinceCode {
constructor(public readonly value: string) {}
static create(value: string): ProvinceCode {
return new ProvinceCode(value || 'DEFAULT');
}
}
export class CityCode {
constructor(public readonly value: string) {}
static create(value: string): CityCode {
return new CityCode(value || 'DEFAULT');
}
}
// ============ Mnemonic ============
export class Mnemonic {
constructor(public readonly value: string) {

View File

@ -7,9 +7,6 @@ export interface UserAccountEntity {
avatarUrl: string | null;
inviterSequence: bigint | null;
referralCode: string;
provinceCode: string;
cityCode: string;
address: string | null;
kycStatus: string;
realName: string | null;
idCardNumber: string | null;

View File

@ -52,9 +52,6 @@ export class UserAccountMapper {
avatarUrl: entity.avatarUrl,
inviterSequence: entity.inviterSequence ? Number(entity.inviterSequence) : null,
referralCode: entity.referralCode,
province: entity.provinceCode,
city: entity.cityCode,
address: entity.address,
walletAddresses: wallets,
kycInfo,
kycStatus: entity.kycStatus as KYCStatus,

View File

@ -32,9 +32,6 @@ export class UserAccountRepositoryImpl implements UserAccountRepository {
avatarUrl: account.avatarUrl,
inviterSequence: account.inviterSequence ? BigInt(account.inviterSequence.value) : null,
referralCode: account.referralCode.value,
provinceCode: account.province.value,
cityCode: account.city.value,
address: account.addressDetail,
kycStatus: account.kycStatus,
realName: account.kycInfo?.realName || null,
idCardNumber: account.kycInfo?.idCardNumber || null,
@ -56,9 +53,6 @@ export class UserAccountRepositoryImpl implements UserAccountRepository {
phoneNumber: account.phoneNumber?.value || null,
nickname: account.nickname,
avatarUrl: account.avatarUrl,
provinceCode: account.province.value,
cityCode: account.city.value,
address: account.addressDetail,
kycStatus: account.kycStatus,
realName: account.kycInfo?.realName || null,
idCardNumber: account.kycInfo?.idCardNumber || null,
@ -184,14 +178,12 @@ export class UserAccountRepositoryImpl implements UserAccountRepository {
}
async findUsers(
filters?: { status?: AccountStatus; kycStatus?: KYCStatus; province?: string; city?: string; keyword?: string },
filters?: { status?: AccountStatus; kycStatus?: KYCStatus; keyword?: string },
pagination?: Pagination,
): Promise<UserAccount[]> {
const where: any = {};
if (filters?.status) where.status = filters.status;
if (filters?.kycStatus) where.kycStatus = filters.kycStatus;
if (filters?.province) where.provinceCode = filters.province;
if (filters?.city) where.cityCode = filters.city;
if (filters?.keyword) {
where.OR = [
{ nickname: { contains: filters.keyword } },
@ -262,9 +254,6 @@ export class UserAccountRepositoryImpl implements UserAccountRepository {
avatarUrl: data.avatarUrl,
inviterSequence: data.inviterSequence ? Number(data.inviterSequence) : null,
referralCode: data.referralCode,
province: data.provinceCode,
city: data.cityCode,
address: data.address,
walletAddresses: wallets,
kycInfo,
kycStatus: data.kycStatus as KYCStatus,