feat: 支持馆主个人信息编辑
This commit is contained in:
@@ -132,6 +132,8 @@ const mockStudioConfig = {
|
||||
longitude: null,
|
||||
cancelHoursLimit: 2,
|
||||
photos: [],
|
||||
ownerIntroduction: '',
|
||||
ownerCertifications: [],
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'
|
||||
import { StudioService } from '../studio.service'
|
||||
import { PrismaService } from '../../prisma/prisma.service'
|
||||
import { UpdateStudioDto } from '../dto/update-studio.dto'
|
||||
import { DEFAULT_OWNER_INTRODUCTION, DEFAULT_OWNER_CERTIFICATIONS } from '@mp-pilates/shared'
|
||||
|
||||
const mockStudioConfig = {
|
||||
id: 'test-id-001',
|
||||
@@ -14,6 +15,8 @@ const mockStudioConfig = {
|
||||
longitude: null,
|
||||
cancelHoursLimit: 2,
|
||||
photos: [],
|
||||
ownerIntroduction: DEFAULT_OWNER_INTRODUCTION,
|
||||
ownerCertifications: [...DEFAULT_OWNER_CERTIFICATIONS],
|
||||
updatedAt: new Date('2024-01-01T00:00:00Z'),
|
||||
}
|
||||
|
||||
@@ -41,6 +44,25 @@ describe('StudioService', () => {
|
||||
})
|
||||
|
||||
describe('getInfo()', () => {
|
||||
it('preserves the original introduction and credentials for an unconfigured studio', async () => {
|
||||
mockPrismaService.studioConfig.findFirst.mockResolvedValue({
|
||||
...mockStudioConfig,
|
||||
ownerIntroduction: null,
|
||||
ownerCertifications: null,
|
||||
})
|
||||
|
||||
expect(await service.getInfo()).toEqual(mockStudioConfig)
|
||||
})
|
||||
|
||||
it('does not restore defaults when the owner intentionally clears content', async () => {
|
||||
mockPrismaService.studioConfig.findFirst.mockResolvedValue({
|
||||
...mockStudioConfig,
|
||||
ownerIntroduction: '',
|
||||
ownerCertifications: [],
|
||||
})
|
||||
|
||||
expect(await service.getInfo()).toMatchObject({ ownerIntroduction: '', ownerCertifications: [] })
|
||||
})
|
||||
it('should return existing config when one exists', async () => {
|
||||
mockPrismaService.studioConfig.findFirst.mockResolvedValue(mockStudioConfig)
|
||||
|
||||
@@ -67,6 +89,42 @@ describe('StudioService', () => {
|
||||
})
|
||||
|
||||
describe('updateInfo()', () => {
|
||||
it('persists owner content with paragraphs and credential order, independently of studio settings', async () => {
|
||||
const dto: UpdateStudioDto = {
|
||||
ownerIntroduction: ' 我的教学经历\n\n我的练习理念 ',
|
||||
ownerCertifications: [' 认证 B ', '', '认证 A', '认证 B'],
|
||||
}
|
||||
const normalized = { ownerIntroduction: '我的教学经历\n\n我的练习理念', ownerCertifications: ['认证 B', '认证 A'] }
|
||||
mockPrismaService.studioConfig.findFirst.mockResolvedValue({ id: mockStudioConfig.id })
|
||||
mockPrismaService.studioConfig.update.mockResolvedValue({ ...mockStudioConfig, ...normalized })
|
||||
|
||||
expect(await service.updateInfo(dto)).toMatchObject(normalized)
|
||||
expect(mockPrismaService.studioConfig.update).toHaveBeenCalledWith({
|
||||
where: { id: mockStudioConfig.id },
|
||||
data: { ...normalized, photos: undefined },
|
||||
})
|
||||
})
|
||||
|
||||
it('saves empty content and does not overwrite owner content when updating other settings', async () => {
|
||||
mockPrismaService.studioConfig.findFirst.mockResolvedValue({ id: mockStudioConfig.id })
|
||||
mockPrismaService.studioConfig.update.mockResolvedValue({ ...mockStudioConfig, ownerIntroduction: '', ownerCertifications: [] })
|
||||
await service.updateInfo({ ownerIntroduction: '', ownerCertifications: [] })
|
||||
expect(mockPrismaService.studioConfig.update.mock.calls[0][0].data).toMatchObject({ ownerIntroduction: '', ownerCertifications: [] })
|
||||
|
||||
await service.updateInfo({ name: '新的馆名' })
|
||||
const data = mockPrismaService.studioConfig.update.mock.calls[1][0].data
|
||||
expect(data.ownerIntroduction).toBeUndefined()
|
||||
expect(data.ownerCertifications).toBeUndefined()
|
||||
})
|
||||
|
||||
it('creates a studio with editable owner content if no config exists yet', async () => {
|
||||
const dto = { ownerIntroduction: '馆主的故事', ownerCertifications: ['认证 A'] }
|
||||
mockPrismaService.studioConfig.findFirst.mockResolvedValue(null)
|
||||
mockPrismaService.studioConfig.create.mockResolvedValue({ ...mockStudioConfig, ...dto })
|
||||
|
||||
expect(await service.updateInfo(dto)).toMatchObject(dto)
|
||||
expect(mockPrismaService.studioConfig.create).toHaveBeenCalledWith({ data: { name: '普拉提工作室', ...dto, photos: undefined } })
|
||||
})
|
||||
it('should update and return a new config object with changes applied', async () => {
|
||||
const dto: UpdateStudioDto = {
|
||||
name: 'New Studio Name',
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { plainToInstance } from 'class-transformer'
|
||||
import { validate } from 'class-validator'
|
||||
import { OWNER_INTRODUCTION_MAX_LENGTH, OWNER_CERTIFICATIONS_MAX_COUNT, OWNER_CERTIFICATION_MAX_LENGTH } from '@mp-pilates/shared'
|
||||
import { UpdateStudioDto } from '../dto/update-studio.dto'
|
||||
|
||||
const errorsFor = (input: Record<string, unknown>) => validate(plainToInstance(UpdateStudioDto, input))
|
||||
|
||||
describe('UpdateStudioDto owner content', () => {
|
||||
it.each([
|
||||
{},
|
||||
{ ownerIntroduction: '', ownerCertifications: [] },
|
||||
{ ownerIntroduction: '我的介绍\n\n我的理念', ownerCertifications: ['认证 A', '认证 B'] },
|
||||
{ ownerIntroduction: '字'.repeat(OWNER_INTRODUCTION_MAX_LENGTH), ownerCertifications: Array(OWNER_CERTIFICATIONS_MAX_COUNT).fill('字'.repeat(OWNER_CERTIFICATION_MAX_LENGTH)) },
|
||||
])('accepts partial updates, intentional clearing and content within limits', async input => {
|
||||
expect(await errorsFor(input)).toHaveLength(0)
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ ownerIntroduction: null },
|
||||
{ ownerIntroduction: 123 },
|
||||
{ ownerIntroduction: '字'.repeat(OWNER_INTRODUCTION_MAX_LENGTH + 1) },
|
||||
{ ownerCertifications: null },
|
||||
{ ownerCertifications: '认证 A' },
|
||||
{ ownerCertifications: [123] },
|
||||
{ ownerCertifications: [null] },
|
||||
{ ownerCertifications: ['字'.repeat(OWNER_CERTIFICATION_MAX_LENGTH + 1)] },
|
||||
{ ownerCertifications: Array(OWNER_CERTIFICATIONS_MAX_COUNT + 1).fill('认证') },
|
||||
])('rejects malformed or oversized content: %j', async input => {
|
||||
expect((await errorsFor(input)).length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
@@ -3,10 +3,17 @@ import {
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUrl,
|
||||
ArrayMaxSize,
|
||||
MaxLength,
|
||||
ValidateIf,
|
||||
Max,
|
||||
Min,
|
||||
} from 'class-validator'
|
||||
import {
|
||||
OWNER_INTRODUCTION_MAX_LENGTH,
|
||||
OWNER_CERTIFICATIONS_MAX_COUNT,
|
||||
OWNER_CERTIFICATION_MAX_LENGTH,
|
||||
} from '@mp-pilates/shared'
|
||||
|
||||
export class UpdateStudioDto {
|
||||
@IsOptional()
|
||||
@@ -50,4 +57,16 @@ export class UpdateStudioDto {
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
photos?: string[]
|
||||
|
||||
@ValidateIf((_object, value) => value !== undefined)
|
||||
@IsString()
|
||||
@MaxLength(OWNER_INTRODUCTION_MAX_LENGTH)
|
||||
ownerIntroduction?: string
|
||||
|
||||
@ValidateIf((_object, value) => value !== undefined)
|
||||
@IsArray()
|
||||
@ArrayMaxSize(OWNER_CERTIFICATIONS_MAX_COUNT)
|
||||
@IsString({ each: true })
|
||||
@MaxLength(OWNER_CERTIFICATION_MAX_LENGTH, { each: true })
|
||||
ownerCertifications?: string[]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { StudioConfig as PrismaStudioConfig } from '@prisma/client'
|
||||
import { DEFAULT_OWNER_INTRODUCTION, DEFAULT_OWNER_CERTIFICATIONS } from '@mp-pilates/shared'
|
||||
import { PrismaService } from '../prisma/prisma.service'
|
||||
import { UpdateStudioDto } from './dto/update-studio.dto'
|
||||
|
||||
@@ -30,7 +31,9 @@ export class StudioService {
|
||||
|
||||
const data = {
|
||||
...dto,
|
||||
photos: dto.photos ? this.normalizePhotos(dto.photos) : undefined,
|
||||
photos: dto.photos ? this.normalizeStringList(dto.photos) : undefined,
|
||||
ownerIntroduction: dto.ownerIntroduction?.trim(),
|
||||
ownerCertifications: dto.ownerCertifications === undefined ? undefined : this.normalizeStringList(dto.ownerCertifications),
|
||||
}
|
||||
|
||||
const record = existing
|
||||
@@ -50,11 +53,15 @@ export class StudioService {
|
||||
...config,
|
||||
latitude: config.latitude == null ? null : Number(config.latitude),
|
||||
longitude: config.longitude == null ? null : Number(config.longitude),
|
||||
photos: this.normalizePhotos(config.photos),
|
||||
photos: this.normalizeStringList(config.photos),
|
||||
ownerIntroduction: config.ownerIntroduction ?? DEFAULT_OWNER_INTRODUCTION,
|
||||
ownerCertifications: config.ownerCertifications == null
|
||||
? [...DEFAULT_OWNER_CERTIFICATIONS]
|
||||
: this.normalizeStringList(config.ownerCertifications),
|
||||
}
|
||||
}
|
||||
|
||||
private normalizePhotos(value: unknown): string[] {
|
||||
private normalizeStringList(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user