feat(deploy): production deployment setup and fixes

- Debian-based api image (bookworm-slim), docker/debian mirrors, prisma
  binaryTargets for openssl 3.0
- nginx: admin SPA under /admin, TLS via acme.sh (ZeroSSL) + auto-renewal
  cron, http->https redirect
- prisma: add origin_goods.delisted migration, sync missing schema
  (good_image/tag_font_color/good_tags), fix users.createdAt Timestamptz
- api: CORS wildcard reflection, helmet CORP cross-origin, price
  backfill in persistProductDetail, categoryIcon ancestor fallback,
  mediaByColor per-color gallery in public goods detail
- admin: /admin base path (vite + router)
- import-data.mjs: udt_name casting, serial sequence advance fix
This commit is contained in:
yeuimu
2026-08-26 14:23:09 +08:00
parent be0b90e68f
commit 6c61a4e871
982 changed files with 74156 additions and 179393 deletions
@@ -1,114 +0,0 @@
---
title: Throw HTTP Exceptions from Services
impact: HIGH
impactDescription: Keeps controllers thin and simplifies error handling
tags: error-handling, exceptions, services
---
## Throw HTTP Exceptions from Services
It's acceptable (and often preferable) to throw `HttpException` subclasses from services in HTTP applications. This keeps controllers thin and allows services to communicate appropriate error states. For truly layer-agnostic services, use domain exceptions that map to HTTP status codes.
**Incorrect (return error objects instead of throwing):**
```typescript
// Return error objects instead of throwing
@Injectable()
export class UsersService {
async findById(id: string): Promise<{ user?: User; error?: string }> {
const user = await this.repo.findOne({ where: { id } });
if (!user) {
return { error: 'User not found' }; // Controller must check this
}
return { user };
}
}
@Controller('users')
export class UsersController {
@Get(':id')
async findOne(@Param('id') id: string) {
const result = await this.usersService.findById(id);
if (result.error) {
throw new NotFoundException(result.error);
}
return result.user;
}
}
```
**Correct (throw exceptions directly from service):**
```typescript
// Throw exceptions directly from service
@Injectable()
export class UsersService {
constructor(private readonly repo: UserRepository) {}
async findById(id: string): Promise<User> {
const user = await this.repo.findOne({ where: { id } });
if (!user) {
throw new NotFoundException(`User #${id} not found`);
}
return user;
}
async create(dto: CreateUserDto): Promise<User> {
const existing = await this.repo.findOne({
where: { email: dto.email },
});
if (existing) {
throw new ConflictException('Email already registered');
}
return this.repo.save(dto);
}
async update(id: string, dto: UpdateUserDto): Promise<User> {
const user = await this.findById(id); // Throws if not found
Object.assign(user, dto);
return this.repo.save(user);
}
}
// Controller stays thin
@Controller('users')
export class UsersController {
@Get(':id')
findOne(@Param('id') id: string): Promise<User> {
return this.usersService.findById(id);
}
@Post()
create(@Body() dto: CreateUserDto): Promise<User> {
return this.usersService.create(dto);
}
}
// For layer-agnostic services, use domain exceptions
export class EntityNotFoundException extends Error {
constructor(
public readonly entity: string,
public readonly id: string,
) {
super(`${entity} with ID "${id}" not found`);
}
}
// Map to HTTP in exception filter
@Catch(EntityNotFoundException)
export class EntityNotFoundFilter implements ExceptionFilter {
catch(exception: EntityNotFoundException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
response.status(404).json({
statusCode: 404,
message: exception.message,
entity: exception.entity,
id: exception.id,
});
}
}
```
Reference: [NestJS Exception Filters](https://docs.nestjs.com/exception-filters)