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,97 +1,97 @@
import { Test } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';
import { PositionsService } from './positions.service';
import { PrismaService } from '../prisma/prisma.service';
describe('PositionsService', () => {
let service: PositionsService;
let prisma: PrismaService;
let countryId: bigint;
let categoryId: bigint;
const createdIds: bigint[] = [];
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [PositionsService, PrismaService],
}).compile();
service = moduleRef.get(PositionsService);
prisma = moduleRef.get(PrismaService);
await prisma.onModuleInit();
const stamp = Date.now();
const c = await prisma.country.create({
data: { countryName: `Pos Country ${stamp}` },
});
countryId = c.id;
const cat = await prisma.category.create({
data: { categoryName: `Pos Cat ${stamp}` },
});
categoryId = cat.id;
});
afterAll(async () => {
if (createdIds.length) {
await prisma.position.deleteMany({ where: { id: { in: createdIds } } });
}
await prisma.country.delete({ where: { id: countryId } });
await prisma.category.delete({ where: { id: categoryId } });
await prisma.onModuleDestroy();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('creates a position linked to country + category and returns joined names', async () => {
const created = await service.create({
indexVal: 1,
countryId: Number(countryId),
categoryId: Number(categoryId),
});
createdIds.push(created.id);
expect(created.country?.countryName).toBeTruthy();
expect(created.category?.categoryName).toBeTruthy();
const fetched = await service.findOne(created.id);
expect(fetched.country?.id).toBe(countryId);
expect(fetched.category?.id).toBe(categoryId);
});
it('filters by countryId and categoryId', async () => {
const list = await service.findAll({
countryId,
categoryId,
});
expect(list.length).toBeGreaterThan(0);
expect(list.every((p) => p.countryId === countryId)).toBe(true);
expect(list.every((p) => p.categoryId === categoryId)).toBe(true);
});
it('updates indexVal and disconnects country', async () => {
const created = await service.create({
indexVal: 5,
countryId: Number(countryId),
});
createdIds.push(created.id);
const updated = await service.update(created.id, {
indexVal: 9,
countryId: null,
});
expect(updated.indexVal).toBe(9);
expect(updated.countryId).toBeNull();
});
it('throws NotFoundException for unknown country during create', async () => {
await expect(
service.create({ indexVal: 1, countryId: 99999999 }),
).rejects.toBeInstanceOf(NotFoundException);
});
it('deletes a position', async () => {
const created = await service.create({ indexVal: 7 });
await service.remove(created.id);
const list = await service.findAll();
expect(list.find((p) => p.id === created.id)).toBeUndefined();
});
});
import { Test } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';
import { PositionsService } from './positions.service';
import { PrismaService } from '../prisma/prisma.service';
describe('PositionsService', () => {
let service: PositionsService;
let prisma: PrismaService;
let countryId: bigint;
let categoryId: bigint;
const createdIds: bigint[] = [];
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [PositionsService, PrismaService],
}).compile();
service = moduleRef.get(PositionsService);
prisma = moduleRef.get(PrismaService);
await prisma.onModuleInit();
const stamp = Date.now();
const c = await prisma.country.create({
data: { countryName: `Pos Country ${stamp}` },
});
countryId = c.id;
const cat = await prisma.category.create({
data: { categoryName: `Pos Cat ${stamp}` },
});
categoryId = cat.id;
});
afterAll(async () => {
if (createdIds.length) {
await prisma.position.deleteMany({ where: { id: { in: createdIds } } });
}
await prisma.country.delete({ where: { id: countryId } });
await prisma.category.delete({ where: { id: categoryId } });
await prisma.onModuleDestroy();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('creates a position linked to country + category and returns joined names', async () => {
const created = await service.create({
indexVal: 1,
countryId: Number(countryId),
categoryId: Number(categoryId),
});
createdIds.push(created.id);
expect(created.country?.countryName).toBeTruthy();
expect(created.category?.categoryName).toBeTruthy();
const fetched = await service.findOne(created.id);
expect(fetched.country?.id).toBe(countryId);
expect(fetched.category?.id).toBe(categoryId);
});
it('filters by countryId and categoryId', async () => {
const list = await service.findAll({
countryId,
categoryId,
});
expect(list.length).toBeGreaterThan(0);
expect(list.every((p) => p.countryId === countryId)).toBe(true);
expect(list.every((p) => p.categoryId === categoryId)).toBe(true);
});
it('updates indexVal and disconnects country', async () => {
const created = await service.create({
indexVal: 5,
countryId: Number(countryId),
});
createdIds.push(created.id);
const updated = await service.update(created.id, {
indexVal: 9,
countryId: null,
});
expect(updated.indexVal).toBe(9);
expect(updated.countryId).toBeNull();
});
it('throws NotFoundException for unknown country during create', async () => {
await expect(
service.create({ indexVal: 1, countryId: 99999999 }),
).rejects.toBeInstanceOf(NotFoundException);
});
it('deletes a position', async () => {
const created = await service.create({ indexVal: 7 });
await service.remove(created.id);
const list = await service.findAll();
expect(list.find((p) => p.id === created.id)).toBeUndefined();
});
});