|
|
@@ -4,6 +4,7 @@ import {
|
|
|
Injectable,
|
|
|
NotFoundException,
|
|
|
} from '@nestjs/common';
|
|
|
+import { randomBytes } from 'crypto';
|
|
|
import { Prisma } from '@prisma/client';
|
|
|
import { PrismaService } from '../../prisma/prisma.service';
|
|
|
import { normalizeTemplateNameI18n } from '../templates/template-name-i18n';
|
|
|
@@ -38,7 +39,7 @@ export class StylistService {
|
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
|
|
async getMyProfile(stylistId: bigint): Promise<StylistProfileDto> {
|
|
|
- const stylist = await this.getOwnedStylist(stylistId);
|
|
|
+ const stylist = await this.ensurePublicIdentity(await this.getOwnedStylist(stylistId));
|
|
|
const [publishedTemplateCount, templates] = await this.getPublishedTemplates(stylist.id);
|
|
|
return this.toProfileDto(stylist, publishedTemplateCount, templates);
|
|
|
}
|
|
|
@@ -48,7 +49,7 @@ export class StylistService {
|
|
|
dto: UpdateStylistProfileDto,
|
|
|
): Promise<StylistProfileDto> {
|
|
|
const stylist = await this.getOwnedStylist(stylistId);
|
|
|
- const slug = this.normalizeSlug(dto.slug, stylist.username);
|
|
|
+ const slug = this.normalizeSlug(dto.slug, stylist.username, stylist.id);
|
|
|
|
|
|
if (isReservedProfileSlug(slug)) {
|
|
|
throw new BadRequestException('该公开主页路径不可用');
|
|
|
@@ -110,9 +111,10 @@ export class StylistService {
|
|
|
throw new NotFoundException('公开主页不存在');
|
|
|
}
|
|
|
|
|
|
- const [publishedTemplateCount, templates] = await this.getPublishedTemplates(stylist.id);
|
|
|
+ const resolved = await this.ensurePublicIdentity(stylist);
|
|
|
+ const [publishedTemplateCount, templates] = await this.getPublishedTemplates(resolved.id);
|
|
|
|
|
|
- const profile = this.toProfileDto(stylist, publishedTemplateCount, templates);
|
|
|
+ const profile = this.toProfileDto(resolved, publishedTemplateCount, templates);
|
|
|
const { email, username, ...publicProfile } = profile;
|
|
|
void email;
|
|
|
void username;
|
|
|
@@ -135,6 +137,7 @@ export class StylistService {
|
|
|
isDeleted: 0,
|
|
|
status: 'PUBLIC' as const,
|
|
|
isSystemTemplate: 'NO' as const,
|
|
|
+ templateUrl: { not: '' },
|
|
|
};
|
|
|
|
|
|
const [publishedTemplateCount, templates] = await Promise.all([
|
|
|
@@ -166,8 +169,77 @@ export class StylistService {
|
|
|
] as const;
|
|
|
}
|
|
|
|
|
|
- private normalizeSlug(slug: string, username: string) {
|
|
|
- return slugifyProfilePath(slug || username) || `stylist`;
|
|
|
+ private async ensurePublicIdentity(stylist: StylistRow): Promise<StylistRow> {
|
|
|
+ if (stylist.slug && stylist.simulationInviteCode) return stylist;
|
|
|
+
|
|
|
+ for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
|
+ const data: Prisma.HairstylistUserUpdateInput = {};
|
|
|
+ if (!stylist.slug) {
|
|
|
+ data.slug = await this.allocateSlug(stylist);
|
|
|
+ }
|
|
|
+ if (!stylist.simulationInviteCode) {
|
|
|
+ data.simulationInviteCode = await this.allocateInviteCode(stylist.id);
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ return await this.prisma.hairstylistUser.update({
|
|
|
+ where: { id: stylist.id },
|
|
|
+ data,
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ if (
|
|
|
+ error instanceof Prisma.PrismaClientKnownRequestError &&
|
|
|
+ error.code === 'P2002'
|
|
|
+ ) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ throw new BadRequestException('暂时无法生成公开主页信息,请稍后重试');
|
|
|
+ }
|
|
|
+
|
|
|
+ private async allocateSlug(stylist: StylistRow): Promise<string> {
|
|
|
+ const fromName = slugifyProfilePath(stylist.username);
|
|
|
+ const base =
|
|
|
+ fromName && !isReservedProfileSlug(fromName)
|
|
|
+ ? fromName
|
|
|
+ : `stylist-${stylist.id.toString()}`;
|
|
|
+
|
|
|
+ for (let index = 0; index < 20; index += 1) {
|
|
|
+ const candidate = index === 0 ? base : `${base}-${index + 1}`;
|
|
|
+ if (isReservedProfileSlug(candidate)) continue;
|
|
|
+ const taken = await this.prisma.hairstylistUser.findFirst({
|
|
|
+ where: { slug: candidate, NOT: { id: stylist.id } },
|
|
|
+ select: { id: true },
|
|
|
+ });
|
|
|
+ if (!taken) return candidate;
|
|
|
+ }
|
|
|
+
|
|
|
+ return `stylist-${stylist.id.toString()}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ private async allocateInviteCode(stylistId: bigint): Promise<string> {
|
|
|
+ for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
|
+ const inviteCode = this.generateInviteCode();
|
|
|
+ const taken = await this.prisma.hairstylistUser.findFirst({
|
|
|
+ where: { simulationInviteCode: inviteCode, NOT: { id: stylistId } },
|
|
|
+ select: { id: true },
|
|
|
+ });
|
|
|
+ if (!taken) return inviteCode;
|
|
|
+ }
|
|
|
+ throw new BadRequestException('暂时无法生成模拟代码,请稍后重试');
|
|
|
+ }
|
|
|
+
|
|
|
+ private generateInviteCode(): string {
|
|
|
+ const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
|
|
+ const bytes = randomBytes(8);
|
|
|
+ return Array.from(bytes, (value) => alphabet[value % alphabet.length]).join('');
|
|
|
+ }
|
|
|
+
|
|
|
+ private normalizeSlug(slug: string, username: string, stylistId: bigint) {
|
|
|
+ return slugifyProfilePath(slug || username) || `stylist-${stylistId.toString()}`;
|
|
|
}
|
|
|
|
|
|
private toProfileDto(
|