diff --git "a/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/nest-cli.json" "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/nest-cli.json" new file mode 100644 index 0000000000000000000000000000000000000000..fc5f4c6aba35cc83a627c70171b1f22f61d764ec --- /dev/null +++ "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/nest-cli.json" @@ -0,0 +1,9 @@ +{ + "sourceRoot": "src", + "compilerOptions": { + "tsConfigPath": "tsconfig.json", + "outDir": "dist", + "assets": [], + "watchAssets": false + } +} diff --git "a/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/package.json" "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/package.json" new file mode 100644 index 0000000000000000000000000000000000000000..082b36143a03a9c91ea87689535a27c4253c82c6 --- /dev/null +++ "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/package.json" @@ -0,0 +1,47 @@ +{ + "name": "@shoppings3/server", + "version": "0.0.0", + "private": true, + "scripts": { + "dev": "nest start --watch", + "build": "nest build", + "start": "node dist/main", + "prisma:generate": "prisma generate", + "prisma:push": "prisma db push" + }, + "dependencies": { + "@nestjs/common": "^10.4.15", + "@nestjs/core": "^10.4.15", + "@nestjs/jwt": "^10.2.0", + "@nestjs/passport": "^10.0.3", + "@nestjs/platform-express": "^10.4.15", + "@prisma/adapter-pg": "^7.9.1", + "@prisma/client": "^7.4.2", + "@prisma/config": "^7.9.1", + "bcrypt": "^5.1.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "ioredis": "^5.4.2", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "pg": "^8.13.1", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@nestjs/cli": "^10.4.9", + "@nestjs/schematics": "^10.2.3", + "@types/bcrypt": "^5.0.2", + "@types/express": "^5.0.0", + "@types/node": "^22.10.5", + "@types/passport-jwt": "^4.0.1", + "@types/pg": "^8.11.10", + "prisma": "^7.4.2", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "~5.7.3" + }, + "prisma": { + "schema": "../../packages/shared/prisma/schema.prisma" + } +} diff --git "a/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/prisma.config.ts" "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/prisma.config.ts" new file mode 100644 index 0000000000000000000000000000000000000000..0906b14fe18e9fd3f2cc50c19b65a9fe73d36ac1 --- /dev/null +++ "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/prisma.config.ts" @@ -0,0 +1,9 @@ +import { defineConfig } from '@prisma/config'; + +export default defineConfig({ + earlyAccess: true, + schema: './packages/shared/prisma/schema.prisma', + datasource: { + url: 'postgresql://mall:mall123456@localhost:5432/mall_db', + }, +}); diff --git "a/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/app.module.ts" "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/app.module.ts" new file mode 100644 index 0000000000000000000000000000000000000000..da89836c57cc4562bf166f57316ed0232d0935fa --- /dev/null +++ "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/app.module.ts" @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { DatabaseModule } from './framework/database/database.module'; +import { RedisModule } from './framework/redis/redis.module'; +import { UserModule } from './modules/user/user.module'; +import { CartModule } from './modules/cart/cart.module'; + +@Module({ + imports: [DatabaseModule, RedisModule, UserModule, CartModule], +}) +export class AppModule {} diff --git "a/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/framework/database/database.module.ts" "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/framework/database/database.module.ts" new file mode 100644 index 0000000000000000000000000000000000000000..aead0c0262cfdef3468b8b4330b9886705d5893b --- /dev/null +++ "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/framework/database/database.module.ts" @@ -0,0 +1,25 @@ +import { Module, Global } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; +import { PrismaPg } from '@prisma/adapter-pg'; +import { Pool } from 'pg'; + +@Global() +@Module({ + providers: [ + { + provide: PrismaClient, + useFactory: () => { + const url = process.env.DATABASE_URL; + const match = url.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/); + if (!match) throw new Error('Invalid DATABASE_URL'); + const [, user, password, host, port, database] = match; + + const pool = new Pool({ user, password, host, port: parseInt(port), database }); + const adapter = new PrismaPg(pool); + return new PrismaClient({ adapter }); + }, + }, + ], + exports: [PrismaClient], +}) +export class DatabaseModule {} diff --git "a/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/framework/exceptions/business.exception.ts" "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/framework/exceptions/business.exception.ts" new file mode 100644 index 0000000000000000000000000000000000000000..54c85001346c2b6e2bcaa33cb76a3075e5f891be --- /dev/null +++ "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/framework/exceptions/business.exception.ts" @@ -0,0 +1,10 @@ +export class BusinessException extends Error { + constructor( + public readonly message: string, + public readonly code: string = 'BUSINESS_ERROR', + public readonly statusCode: number = 400, + ) { + super(message); + this.name = 'BusinessException'; + } +} diff --git "a/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/framework/redis/redis.module.ts" "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/framework/redis/redis.module.ts" new file mode 100644 index 0000000000000000000000000000000000000000..ef7c808cb339d60e0174f7ae37e36e7e95c7196c --- /dev/null +++ "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/framework/redis/redis.module.ts" @@ -0,0 +1,9 @@ +import { Module, Global } from '@nestjs/common'; +import { RedisService } from './redis.service'; + +@Global() +@Module({ + providers: [RedisService], + exports: [RedisService], +}) +export class RedisModule {} diff --git "a/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/framework/redis/redis.service.ts" "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/framework/redis/redis.service.ts" new file mode 100644 index 0000000000000000000000000000000000000000..4f54ce0659f22f9aa759ed0183cf0ba1e508a2cc --- /dev/null +++ "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/framework/redis/redis.service.ts" @@ -0,0 +1,37 @@ +// apps/server/src/framework/redis/redis.service.ts + +import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; +import Redis from 'ioredis'; + +@Injectable() +export class RedisService implements OnModuleInit, OnModuleDestroy { + private client: Redis; + + async onModuleInit() { + this.client = new Redis({ + host: process.env.REDIS_HOST || 'localhost', + port: parseInt(process.env.REDIS_PORT || '6379'), + password: process.env.REDIS_PASSWORD || 'redis123456', // 默认密码 + }); + } + + async onModuleDestroy() { + await this.client?.quit(); + } + + async set(key: string, value: string, ttl?: number): Promise { + if (ttl) { + await this.client.setex(key, ttl, value); + } else { + await this.client.set(key, value); + } + } + + async get(key: string): Promise { + return this.client.get(key); + } + + async del(key: string): Promise { + return this.client.del(key); + } +} \ No newline at end of file diff --git "a/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/main.ts" "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/main.ts" new file mode 100644 index 0000000000000000000000000000000000000000..a3e81d879156c9be3e9a46450054840eaa898618 --- /dev/null +++ "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/main.ts" @@ -0,0 +1,34 @@ +// apps/server/src/main.ts +// 在所有 ES 模块导入之前加载 .env(使用 CommonJS) +const fs = require('fs'); +const path = require('path'); +const possiblePaths = [path.join(__dirname, '.env'), path.join(__dirname, '..', '.env')]; +for (const envPath of possiblePaths) { + if (fs.existsSync(envPath)) { + fs.readFileSync(envPath, 'utf8').split('\n').forEach(line => { + const t = line.trim(); + if (!t || t.startsWith('#')) return; + const eq = t.indexOf('='); + if (eq < 0) return; + let v = t.slice(eq + 1).trim(); + if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1); + if (t.slice(0, eq).trim()) process.env[t.slice(0, eq).trim()] = v; + }); + break; + } +} + +import 'reflect-metadata'; +import { NestFactory } from '@nestjs/core'; +import { ValidationPipe } from '@nestjs/common'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + app.setGlobalPrefix('api'); + app.enableCors(); + app.useGlobalPipes(new ValidationPipe({ transform: true })); + await app.listen(3000); + console.log('🚀 Server running on http://localhost:3000'); +} +bootstrap(); diff --git "a/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/cart/cart.controller.ts" "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/cart/cart.controller.ts" new file mode 100644 index 0000000000000000000000000000000000000000..650439735f0af81c70e100aae353c12f3a74bee8 --- /dev/null +++ "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/cart/cart.controller.ts" @@ -0,0 +1,70 @@ +import { + Controller, + Get, + Post, + Patch, + Delete, + Body, + Param, + UseGuards, + Request, + HttpCode, + HttpStatus, +} from '@nestjs/common'; +import { CartService } from './cart.service'; +import { AddCartItemDto, UpdateCartItemDto, SelectAllDto } from './cart.dto'; +import { JwtAuthGuard } from '../user/guards/jwt-auth.guard'; + +@Controller('cart') +@UseGuards(JwtAuthGuard) +export class CartController { + constructor(private readonly cartService: CartService) {} + + /** 获取当前用户购物车 */ + @Get() + async getCart(@Request() req: any) { + return this.cartService.getCart(req.user.sub); + } + + /** 添加商品到购物车 */ + @Post('items') + async addItem(@Request() req: any, @Body() dto: AddCartItemDto) { + return this.cartService.addItem(req.user.sub, dto); + } + + /** 更新购物车明细(数量/选中状态) */ + @Patch('items/:id') + async updateItem( + @Request() req: any, + @Param('id') id: string, + @Body() dto: UpdateCartItemDto, + ) { + return this.cartService.updateItem(req.user.sub, id, dto); + } + + /** 删除单条购物车明细 */ + @Delete('items/:id') + async deleteItem(@Request() req: any, @Param('id') id: string) { + return this.cartService.deleteItem(req.user.sub, id); + } + + /** 清空购物车 */ + @Delete('items') + @HttpCode(HttpStatus.OK) + async clearCart(@Request() req: any) { + return this.cartService.clearCart(req.user.sub); + } + + /** 全选/全不选 */ + @Post('items/select-all') + @HttpCode(HttpStatus.OK) + async selectAll(@Request() req: any, @Body() dto: SelectAllDto) { + return this.cartService.selectAll(req.user.sub, dto); + } + + /** 获取购物车摘要 */ + @Get('summary') + async getSummary(@Request() req: any) { + return this.cartService.getSummary(req.user.sub); + } +} diff --git "a/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/cart/cart.dto.ts" "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/cart/cart.dto.ts" new file mode 100644 index 0000000000000000000000000000000000000000..ea6c7ee47fda1a51005280ae859e387392360a91 --- /dev/null +++ "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/cart/cart.dto.ts" @@ -0,0 +1,30 @@ +import { IsInt, IsOptional, IsBoolean, IsString, Min } from 'class-validator'; + +/** 添加商品到购物车 */ +export class AddCartItemDto { + @IsString() + productId: string; + + @IsOptional() + @IsInt({ message: '数量必须是整数' }) + @Min(1, { message: '数量最少为1' }) + quantity?: number = 1; +} + +/** 更新购物车明细 */ +export class UpdateCartItemDto { + @IsOptional() + @IsInt({ message: '数量必须是整数' }) + @Min(1, { message: '数量最少为1' }) + quantity?: number; + + @IsOptional() + @IsBoolean({ message: '选中状态必须是布尔值' }) + selected?: boolean; +} + +/** 全选/全不选 */ +export class SelectAllDto { + @IsBoolean({ message: '选中状态必须是布尔值' }) + selected: boolean; +} diff --git "a/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/cart/cart.module.ts" "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/cart/cart.module.ts" new file mode 100644 index 0000000000000000000000000000000000000000..d18d119cb0608ebfa7a95fc860bbd25075a794f6 --- /dev/null +++ "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/cart/cart.module.ts" @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { CartController } from './cart.controller'; +import { CartService } from './cart.service'; + +@Module({ + imports: [ + JwtModule.registerAsync({ + useFactory: () => ({ + secret: process.env.JWT_SECRET || 'dev-secret-key', + signOptions: { expiresIn: '2h' }, + }), + }), + ], + controllers: [CartController], + providers: [CartService], + exports: [CartService], +}) +export class CartModule {} diff --git "a/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/cart/cart.service.ts" "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/cart/cart.service.ts" new file mode 100644 index 0000000000000000000000000000000000000000..1019d7fb139773945f36ee45c7e0b2e3fb0965d1 --- /dev/null +++ "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/cart/cart.service.ts" @@ -0,0 +1,175 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; +import { BusinessException } from '../../framework/exceptions/business.exception'; +import { AddCartItemDto, UpdateCartItemDto, SelectAllDto } from './cart.dto'; + +@Injectable() +export class CartService { + constructor(private readonly prisma: PrismaClient) {} + + // ============================================================================ + // 购物车基础方法 + // ============================================================================ + + /** 获取用户购物车(不存在则自动创建) */ + async getCart(userId: string) { + let cart = await this.prisma.cart.findUnique({ + where: { userId }, + include: { + items: { + include: { + product: true, + }, + orderBy: { createdAt: 'desc' }, + }, + }, + }); + + if (!cart) { + cart = await this.prisma.cart.create({ + data: { userId, status: 'ACTIVE' }, + include: { + items: { + include: { product: true }, + orderBy: { createdAt: 'desc' }, + }, + }, + }); + } + + return cart; + } + + /** 添加商品到购物车 */ + async addItem(userId: string, dto: AddCartItemDto) { + const quantity = dto.quantity ?? 1; + + // 查询商品信息 + const product = await this.prisma.product.findUnique({ where: { id: dto.productId } }); + if (!product) throw new BusinessException('商品不存在', 'PRODUCT_NOT_FOUND', 404); + if (product.status !== 'ON_SHELF') throw new BusinessException('商品已下架', 'PRODUCT_OFF_SHELF'); + if (product.stock < quantity) throw new BusinessException('库存不足', 'INSUFFICIENT_STOCK'); + + // 获取或创建购物车 + let cart = await this.prisma.cart.findUnique({ where: { userId } }); + if (!cart) { + cart = await this.prisma.cart.create({ + data: { userId, status: 'ACTIVE' }, + }); + } + + // 检查是否已存在相同商品 + const existingItem = await this.prisma.cartItem.findFirst({ + where: { cartId: cart.id, productId: String(dto.productId) }, + }); + + if (existingItem) { + // 累加数量 + return this.prisma.cartItem.update({ + where: { id: existingItem.id }, + data: { quantity: existingItem.quantity + quantity }, + }); + } + + // 新增商品,使用商品真实价格 + const priceSnapshot = String(product.price); + + return this.prisma.cartItem.create({ + data: { + cartId: cart.id, + productId: String(dto.productId), + quantity, + priceSnapshot, + selected: true, + }, + }); + } + + /** 更新购物车明细(数量/选中状态) */ + async updateItem(userId: string, itemId: string, dto: UpdateCartItemDto) { + // 校验归属 + const item = await this.prisma.cartItem.findFirst({ + where: { id: itemId, cart: { userId } }, + }); + if (!item) { + throw new BusinessException('购物车明细不存在', 'CART_ITEM_NOT_FOUND', 404); + } + + return this.prisma.cartItem.update({ + where: { id: itemId }, + data: dto, + }); + } + + /** 删除单条购物车明细 */ + async deleteItem(userId: string, itemId: string) { + const item = await this.prisma.cartItem.findFirst({ + where: { id: itemId, cart: { userId } }, + }); + if (!item) { + throw new BusinessException('购物车明细不存在', 'CART_ITEM_NOT_FOUND', 404); + } + + await this.prisma.cartItem.delete({ where: { id: itemId } }); + return { ok: true }; + } + + /** 清空购物车 */ + async clearCart(userId: string) { + const cart = await this.prisma.cart.findUnique({ where: { userId } }); + if (!cart) return { ok: true }; + + await this.prisma.cartItem.deleteMany({ where: { cartId: cart.id } }); + return { ok: true }; + } + + /** 全选/全不选 */ + async selectAll(userId: string, dto: SelectAllDto) { + const cart = await this.prisma.cart.findUnique({ where: { userId } }); + if (!cart) return { ok: true }; + + await this.prisma.cartItem.updateMany({ + where: { cartId: cart.id }, + data: { selected: dto.selected }, + }); + + return { ok: true }; + } + + /** 获取购物车摘要 */ + async getSummary(userId: string) { + const cart = await this.prisma.cart.findUnique({ + where: { userId }, + include: { + items: true, + }, + }); + + if (!cart || cart.items.length === 0) { + return { + totalQuantity: 0, + selectedQuantity: 0, + selectedAmount: '0', + itemCount: 0, + }; + } + + const items = cart.items; + const totalQuantity = items.reduce((sum, item) => sum + item.quantity, 0); + const selectedItems = items.filter((item) => item.selected); + const selectedQuantity = selectedItems.reduce((sum, item) => sum + item.quantity, 0); + + // 计算选中金额(分) + const selectedAmount = selectedItems.reduce((sum, item) => { + const price = parseFloat(item.priceSnapshot || '0'); + return sum + price * item.quantity; + }, 0); + + return { + totalQuantity, + selectedQuantity, + selectedAmount: String(Math.round(selectedAmount)), + itemCount: items.length, + }; + } +} diff --git "a/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/user/guards/jwt-auth.guard.ts" "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/user/guards/jwt-auth.guard.ts" new file mode 100644 index 0000000000000000000000000000000000000000..ea2d6dfbd56a5765e594b0e1fbd53b4a24ef25ea --- /dev/null +++ "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/user/guards/jwt-auth.guard.ts" @@ -0,0 +1,31 @@ +import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; + +@Injectable() +export class JwtAuthGuard { + constructor(private readonly jwtService: JwtService) {} + + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + const token = this.extractTokenFromHeader(request); + + if (!token) { + throw new UnauthorizedException('未提供认证令牌'); + } + + try { + const payload = this.jwtService.verify(token); + request.user = payload; + return true; + } catch (err) { + throw new UnauthorizedException('令牌无效或已过期'); + } + } + + private extractTokenFromHeader(request: any): string | undefined { + const authHeader = request.headers.authorization; + if (!authHeader) return undefined; + const [type, token] = authHeader.split(' '); + return type === 'Bearer' ? token : undefined; + } +} diff --git "a/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/user/user.controller.ts" "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/user/user.controller.ts" new file mode 100644 index 0000000000000000000000000000000000000000..739887d3eb6d3f6fd218f5ffc8f85cbe2053897a --- /dev/null +++ "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/user/user.controller.ts" @@ -0,0 +1,107 @@ +import { + Controller, + Post, + Get, + Put, + Delete, + Patch, + Body, + Param, + UseGuards, + Request, + HttpCode, + HttpStatus, +} from '@nestjs/common'; +import { UserService } from './user.service'; +import { RegisterDto, LoginAccountDto, LoginSmsDto, SendSmsDto, CreateAddressDto, UpdateAddressDto } from './user.dto'; +import { JwtAuthGuard } from './guards/jwt-auth.guard'; + +@Controller() +export class UserController { + constructor(private readonly userService: UserService) {} + + // ============================================================================ + // 认证接口 + // ============================================================================ + + /** 注册 */ + @Post('auth/register') + async register(@Body() dto: RegisterDto) { + return this.userService.register(dto); + } + + /** 账号密码登录 */ + @Post('auth/login-account') + @HttpCode(HttpStatus.OK) + async loginAccount(@Body() dto: LoginAccountDto) { + return this.userService.loginAccount(dto); + } + + /** 短信验证码登录 */ + @Post('auth/login-sms') + @HttpCode(HttpStatus.OK) + async loginSms(@Body() dto: LoginSmsDto) { + return this.userService.loginSms(dto); + } + + /** 发送验证码 */ + @Post('auth/send-sms') + @HttpCode(HttpStatus.OK) + async sendSms(@Body() dto: SendSmsDto) { + return this.userService.sendSms(dto.phone, dto.type); + } + + /** 退出登录 */ + @Post('auth/logout') + @UseGuards(JwtAuthGuard) + @HttpCode(HttpStatus.OK) + async logout() { + return { ok: true }; + } + + /** 获取当前用户 */ + @Get('auth/me') + @UseGuards(JwtAuthGuard) + async getMe(@Request() req: any) { + return this.userService.getMe(req.user.sub); + } + + // ============================================================================ + // 地址接口 + // ============================================================================ + + /** 获取地址列表 */ + @Get('users/addresses') + @UseGuards(JwtAuthGuard) + async getAddresses(@Request() req: any) { + return this.userService.getAddresses(req.user.sub); + } + + /** 新增地址 */ + @Post('users/addresses') + @UseGuards(JwtAuthGuard) + async createAddress(@Request() req: any, @Body() dto: CreateAddressDto) { + return this.userService.createAddress(req.user.sub, dto); + } + + /** 更新地址 */ + @Put('users/addresses/:id') + @UseGuards(JwtAuthGuard) + async updateAddress(@Request() req: any, @Param('id') id: string, @Body() dto: UpdateAddressDto) { + return this.userService.updateAddress(req.user.sub, id, dto); + } + + /** 删除地址 */ + @Delete('users/addresses/:id') + @UseGuards(JwtAuthGuard) + async deleteAddress(@Request() req: any, @Param('id') id: string) { + return this.userService.deleteAddress(req.user.sub, id); + } + + /** 设为默认地址 */ + @Patch('users/addresses/:id/default') + @UseGuards(JwtAuthGuard) + async setDefaultAddress(@Request() req: any, @Param('id') id: string) { + return this.userService.setDefaultAddress(req.user.sub, id); + } +} diff --git "a/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/user/user.dto.ts" "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/user/user.dto.ts" new file mode 100644 index 0000000000000000000000000000000000000000..c7ebaf06a376f158ec21c131cedad3a834d18e21 --- /dev/null +++ "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/user/user.dto.ts" @@ -0,0 +1,141 @@ +import { + IsNotEmpty, + IsString, + IsOptional, + IsBoolean, + IsNumber, + MinLength, + MaxLength, + IsPhoneNumber, + IsEnum, +} from 'class-validator'; + +// ============================================================================ +// 认证 DTO +// ============================================================================ + +/** 注册 */ +export class RegisterDto { + @IsNotEmpty({ message: '用户名不能为空' }) + @IsString() + @MinLength(3, { message: '用户名至少3个字符' }) + @MaxLength(20, { message: '用户名最多20个字符' }) + username: string; + + @IsNotEmpty({ message: '手机号不能为空' }) + @IsString() + phone: string; + + @IsNotEmpty({ message: '密码不能为空' }) + @IsString() + @MinLength(6, { message: '密码至少6个字符' }) + password: string; + + @IsNotEmpty({ message: '确认密码不能为空' }) + @IsString() + confirmPassword: string; + + @IsNotEmpty({ message: '验证码不能为空' }) + @IsString() + @MaxLength(6, { message: '验证码最多6位' }) + smsCode: string; +} + +/** 账号密码登录 */ +export class LoginAccountDto { + @IsNotEmpty({ message: '账号不能为空' }) + @IsString() + identifier: string; // 用户名或手机号 + + @IsNotEmpty({ message: '密码不能为空' }) + @IsString() + password: string; +} + +/** 短信验证码登录 */ +export class LoginSmsDto { + @IsNotEmpty({ message: '手机号不能为空' }) + @IsString() + phone: string; + + @IsNotEmpty({ message: '验证码不能为空' }) + @IsString() + @MaxLength(6, { message: '验证码最多6位' }) + code: string; +} + +/** 发送验证码 */ +export class SendSmsDto { + @IsNotEmpty({ message: '手机号不能为空' }) + @IsString() + phone: string; + + @IsEnum(['login', 'register'], { message: 'type 必须是 login 或 register' }) + type: 'login' | 'register'; +} + +// ============================================================================ +// 地址 DTO +// ============================================================================ + +/** 创建地址 */ +export class CreateAddressDto { + @IsNotEmpty({ message: '收货人姓名不能为空' }) + @IsString() + receiverName: string; + + @IsNotEmpty({ message: '手机号不能为空' }) + @IsString() + phone: string; + + @IsNotEmpty({ message: '省份不能为空' }) + @IsString() + province: string; + + @IsNotEmpty({ message: '城市不能为空' }) + @IsString() + city: string; + + @IsNotEmpty({ message: '区县不能为空' }) + @IsString() + district: string; + + @IsNotEmpty({ message: '详细地址不能为空' }) + @IsString() + detail: string; + + @IsOptional() + @IsBoolean() + isDefault?: boolean; +} + +/** 更新地址 */ +export class UpdateAddressDto { + @IsOptional() + @IsString() + receiverName?: string; + + @IsOptional() + @IsString() + phone?: string; + + @IsOptional() + @IsString() + province?: string; + + @IsOptional() + @IsString() + city?: string; + + @IsOptional() + @IsString() + district?: string; + + @IsOptional() + @IsString() + detail?: string; + + @IsOptional() + @IsBoolean() + isDefault?: boolean; +} diff --git "a/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/user/user.module.ts" "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/user/user.module.ts" new file mode 100644 index 0000000000000000000000000000000000000000..7bd26de784d5f4ad93b7985532f2fd0d4169bc8f --- /dev/null +++ "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/user/user.module.ts" @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +import { UserController } from './user.controller'; +import { UserService } from './user.service'; +import { JwtAuthGuard } from './guards/jwt-auth.guard'; + +@Module({ + imports: [ + PassportModule, + JwtModule.registerAsync({ + useFactory: () => ({ + secret: process.env.JWT_SECRET || 'dev-secret-key', + signOptions: { expiresIn: '2h' }, + }), + }), + ], + controllers: [UserController], + providers: [UserService, JwtAuthGuard], + exports: [UserService], +}) +export class UserModule {} diff --git "a/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/user/user.service.ts" "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/user/user.service.ts" new file mode 100644 index 0000000000000000000000000000000000000000..a9187310ec3b35446548a20c4ad41d64e85ce437 --- /dev/null +++ "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/src/modules/user/user.service.ts" @@ -0,0 +1,290 @@ +import { Injectable } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { PrismaClient } from '@prisma/client'; +import { RedisService } from '../../framework/redis/redis.service'; +import { BusinessException } from '../../framework/exceptions/business.exception'; +import { RegisterDto, LoginAccountDto, LoginSmsDto, CreateAddressDto, UpdateAddressDto } from './user.dto'; +import * as bcrypt from 'bcrypt'; + +const SALT_ROUNDS = 10; +const JWT_EXPIRES_IN = '2h'; +const SMS_CODE_TTL = 300; // 5分钟 +const DEV_SMS_CODE = '123456'; // 开发环境 mock 验证码 + +@Injectable() +export class UserService { + constructor( + private readonly jwtService: JwtService, + private readonly prisma: PrismaClient, + private readonly redisService: RedisService, + ) {} + + // ============================================================================ + // 认证方法 + // ============================================================================ + + /** 注册 */ + async register(dto: RegisterDto) { + // 校验密码确认 + if (dto.password !== dto.confirmPassword) { + throw new BusinessException('两次密码输入不一致', 'PASSWORD_MISMATCH'); + } + + // 校验验证码(开发环境 mock) + const cachedCode = await this.redisService.get(`sms:code:${dto.phone}`); + if (cachedCode !== DEV_SMS_CODE && dto.smsCode !== DEV_SMS_CODE) { + throw new BusinessException('验证码错误或已过期', 'INVALID_SMS_CODE'); + } + + // 检查用户名是否已存在 + const existingUser = await this.prisma.user.findFirst({ + where: { + OR: [{ username: dto.username }, { phone: dto.phone }], + }, + }); + if (existingUser) { + throw new BusinessException('用户名或手机号已被注册', 'USER_EXISTS'); + } + + // 密码哈希 + const hashedPassword = await bcrypt.hash(dto.password, SALT_ROUNDS); + + // 创建用户(事务) + const user = await this.prisma.$transaction(async (tx) => { + const newUser = await tx.user.create({ + data: { + username: dto.username, + phone: dto.phone, + password: hashedPassword, + roles: ['user'], + status: 'active', + }, + }); + + // 为新用户创建购物车 + await tx.cart.create({ + data: { + userId: newUser.id, + status: 'ACTIVE', + }, + }); + + return newUser; + }); + + // 删除验证码 + await this.redisService.del(`sms:code:${dto.phone}`); + + return this.buildAuthResponse(user); + } + + /** 账号密码登录 */ + async loginAccount(dto: LoginAccountDto) { + const user = await this.prisma.user.findFirst({ + where: { + OR: [{ username: dto.identifier }, { phone: dto.identifier }], + }, + }); + + if (!user) { + throw new BusinessException('账号或密码错误', 'INVALID_CREDENTIALS', 401); + } + + const passwordMatch = await bcrypt.compare(dto.password, user.password); + if (!passwordMatch) { + throw new BusinessException('账号或密码错误', 'INVALID_CREDENTIALS', 401); + } + + return this.buildAuthResponse(user); + } + + /** 短信验证码登录 */ + async loginSms(dto: LoginSmsDto) { + const cachedCode = await this.redisService.get(`sms:code:${dto.phone}`); + if (cachedCode !== DEV_SMS_CODE && dto.code !== DEV_SMS_CODE) { + throw new BusinessException('验证码错误或已过期', 'INVALID_SMS_CODE'); + } + + let user = await this.prisma.user.findUnique({ where: { phone: dto.phone } }); + + // 如果用户不存在,自动注册(开发环境行为) + if (!user) { + const hashedPassword = await bcrypt.hash(DEV_SMS_CODE, SALT_ROUNDS); + user = await this.prisma.$transaction(async (tx) => { + const newUser = await tx.user.create({ + data: { + username: `user_${dto.phone.slice(-8)}`, + phone: dto.phone, + password: hashedPassword, + roles: ['user'], + status: 'active', + }, + }); + await tx.cart.create({ + data: { + userId: newUser.id, + status: 'ACTIVE', + }, + }); + return newUser; + }); + } + + // 删除验证码 + await this.redisService.del(`sms:code:${dto.phone}`); + + return this.buildAuthResponse(user); + } + + /** 获取当前用户 */ + async getMe(userId: string) { + const user = await this.prisma.user.findUnique({ + where: { id: userId }, + }); + if (!user) { + throw new BusinessException('用户不存在', 'USER_NOT_FOUND', 404); + } + return this.buildPublicUser(user); + } + + // ============================================================================ + // 地址方法 + // ============================================================================ + + /** 获取地址列表 */ + async getAddresses(userId: string) { + return this.prisma.address.findMany({ + where: { userId }, + orderBy: [{ isDefault: 'desc' }, { createdAt: 'desc' }], + }); + } + + /** 创建地址 */ + async createAddress(userId: string, dto: CreateAddressDto) { + return this.prisma.$transaction(async (tx) => { + // 如果设为默认,先取消其他默认 + if (dto.isDefault) { + await tx.address.updateMany({ + where: { userId, isDefault: true }, + data: { isDefault: false }, + }); + } + + return tx.address.create({ + data: { + userId, + receiverName: dto.receiverName, + phone: dto.phone, + province: dto.province, + city: dto.city, + district: dto.district, + detail: dto.detail, + isDefault: dto.isDefault ?? false, + }, + }); + }); + } + + /** 更新地址 */ + async updateAddress(userId: string, addressId: string, dto: UpdateAddressDto) { + // 校验地址归属 + const address = await this.prisma.address.findFirst({ + where: { id: addressId, userId }, + }); + if (!address) { + throw new BusinessException('地址不存在', 'ADDRESS_NOT_FOUND', 404); + } + + return this.prisma.$transaction(async (tx) => { + // 如果设为默认,先取消其他默认 + if (dto.isDefault) { + await tx.address.updateMany({ + where: { userId, isDefault: true, id: { not: addressId } }, + data: { isDefault: false }, + }); + } + + return tx.address.update({ + where: { id: addressId }, + data: dto, + }); + }); + } + + /** 删除地址 */ + async deleteAddress(userId: string, addressId: string) { + const address = await this.prisma.address.findFirst({ + where: { id: addressId, userId }, + }); + if (!address) { + throw new BusinessException('地址不存在', 'ADDRESS_NOT_FOUND', 404); + } + + await this.prisma.address.delete({ where: { id: addressId } }); + return { ok: true }; + } + + /** 设为默认地址 */ + async setDefaultAddress(userId: string, addressId: string) { + const address = await this.prisma.address.findFirst({ + where: { id: addressId, userId }, + }); + if (!address) { + throw new BusinessException('地址不存在', 'ADDRESS_NOT_FOUND', 404); + } + + await this.prisma.$transaction(async (tx) => { + // 取消所有默认 + await tx.address.updateMany({ + where: { userId, isDefault: true }, + data: { isDefault: false }, + }); + // 设置新默认 + await tx.address.update({ + where: { id: addressId }, + data: { isDefault: true }, + }); + }); + + return { ok: true }; + } + + // ============================================================================ + // 内部方法 + // ============================================================================ + + /** 发送验证码(mock 实现) */ + async sendSms(phone: string, type: 'login' | 'register') { + // 开发环境:验证码固定为 "123456" + await this.redisService.set(`sms:code:${phone}`, DEV_SMS_CODE, SMS_CODE_TTL); + return { + code: DEV_SMS_CODE, // 开发环境返回真实码 + ttlSec: SMS_CODE_TTL, + }; + } + + /** 构建登录响应 */ + private buildAuthResponse(user: any) { + const payload = { sub: user.id, username: user.username, roles: user.roles }; + const accessToken = this.jwtService.sign(payload); + return { + accessToken, + accessExpiresIn: 7200, // 2小时 + user: this.buildPublicUser(user), + }; + } + + /** 构建公开用户信息 */ + private buildPublicUser(user: any) { + return { + id: user.id, + username: user.username, + phone: user.phone, + email: user.email, + avatar: user.avatar, + roles: user.roles, + status: user.status, + createdAt: user.createdAt?.toISOString(), + }; + } +} diff --git "a/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/tsconfig.json" "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/tsconfig.json" new file mode 100644 index 0000000000000000000000000000000000000000..79f28676c4773a32a2713c51a170aefcf3d25f0c --- /dev/null +++ "b/backend/\347\224\250\346\210\267\350\264\255\347\211\251\350\275\246\346\250\241\345\235\227/tsconfig.json" @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "strict": false, + "noImplicitAny": false, + "skipLibCheck": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "outDir": "dist", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "forceConsistentCasingInFileNames": true, + "incremental": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git "a/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-22.md" "b/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-22.md" new file mode 100644 index 0000000000000000000000000000000000000000..030051e3ebee98dbdbc1b1e261531050704d53bd --- /dev/null +++ "b/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-22.md" @@ -0,0 +1,25 @@ +# 日报 - 2026-07-22 + +**成员**:钟咏琪(全栈开发 ) + +## 今日完成 + +- 任务1(研究一下技术栈,做电子商城需要做什么技术栈) + +## 今日计划 + +- 任务2(进行原型设计) + +## 阻塞与求助 + +- 阻塞(无) + +## 明日计划 + +- 任务5(决定每个人需要干什么模块) + +## 进度截图 + +这是我们的设计原型 + +![image-20260724143654375](https://gitee.com/zhongyongqi1121/senlin/raw/master/images/image-20260724143654375.png) \ No newline at end of file diff --git "a/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-23.md" "b/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-23.md" new file mode 100644 index 0000000000000000000000000000000000000000..c7e6b882c70fafb03f24336555ddfae4fd94a4e4 --- /dev/null +++ "b/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-23.md" @@ -0,0 +1,35 @@ +# 日报 - 2026-07-23 + +**成员**:钟咏琪(全栈开发 ) + +## 今日完成 + +- 任务1(确定负责的是用户+购物车模块) + + [ + + ``` + F01 用户注册(完全负责) + F02 用户登录/退出(完全负责) + F03 个人信息与收货地址(完全负责) + F07 购物车(完全负责) + F13 后台用户管理(提供接口,配合后台开发) + ``` + + ] + +## 今日计划 + +- 任务2(了解需要怎么做) + +## 阻塞与求助 + +- 阻塞(无) + +## 明日计划 + +- 任务5(开始F01) + +## 进度截图 + +![屏幕截图 2026-07-24 144323](https://gitee.com/zhongyongqi1121/senlin/raw/master/images/屏幕截图 2026-07-24 144323.png) \ No newline at end of file diff --git "a/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-24.md" "b/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-24.md" new file mode 100644 index 0000000000000000000000000000000000000000..895af11ea508bede4af3593c539778ddc97ce1b6 --- /dev/null +++ "b/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-24.md" @@ -0,0 +1,33 @@ +# 日报 - 2026-07-24 + +**成员**:钟咏琪(全栈开发 ) + +## 今日完成 + +- ☑ Docker Desktop 安装与配置,完成 PostgreSQL 16 + Redis 7.2 容器部署 +- ☑ 配置 Docker 国内镜像加速器,解决镜像拉取超时问题 +- ☑ Prisma 降级至 5.22.0,完成 `users` 表迁移与数据库初始化 +- ☑ NestJS 项目初始化 (`nest new . --skip-git`),项目编译 0 错误通过 +- ☑ 安装后端核心依赖:bcrypt、class-validator、@nestjs/jwt、passport 等 +- ☑ 确认 PostgreSQL + Redis 容器正常运行,数据库连接验证通过 + + +## 今日计划 + +- □ 完成 F01 用户注册页面 UI 设计(Vue3 + Element Plus) +- □ 完成注册页面的前端表单校验逻辑 +- □ 与后端注册接口进行联调准备 + +## 阻塞与求助 + +- 阻塞(Docker Desktop配置汉化包,找不到app.asar,这个文件,无法配置) + +## 明日计划 + +- □ 完成后端 F01 注册接口开发(NestJS + Prisma) +- □ 完成前端注册页面与接口联调 +- □ 开始 F02 登录功能的前后端设计 + +## 进度截图 + +![image-20260724145244784](https://gitee.com/zhongyongqi1121/senlin/raw/master/images/image-20260724145244784.png) \ No newline at end of file diff --git "a/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-27.md" "b/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-27.md" new file mode 100644 index 0000000000000000000000000000000000000000..d45f8860c7283d712e2b0563d7eef91b615128b3 --- /dev/null +++ "b/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-27.md" @@ -0,0 +1,21 @@ +# 日报 - 2026-07-27 + +**成员**:钟咏琪(全栈开发 ) + +## 今日完成 + +- 搭建 Docker 容器环境:编写 docker-compose.yml,启动 PostgreSQL(端口 5432)和 Redis(端口 6379)容器,配置环境变量 POSTGRES_USER=mall、POSTGRES_PASSWORD=mall123456、POSTGRES_DB=mall_db,Redis 密码 redis123456,配置健康检查和持久化卷 +- 搭建 Monorepo 骨架:创建 pnpm-workspace.yaml、turbo.json、.eslintrc.js,生成 apps/web 和 apps/server 目录结构,配置根目录 package.json 使用 pnpm workspace + turbo +- 是想要重新搭建一个新的,之前的不满意 + +## 阻塞与求助 + +## 明日计划 + +- 初始化 NestJS 项目,生成核心配置文件 +- 创建 Prisma Schema,设计 User、Address、Cart、CartItem 数据模型 +- 搭建框架层(DatabaseModule、RedisModule、BusinessException) + +## 进度截图 + +![屏幕截图 2026-07-24 151822](https://gitee.com/zhongyongqi1121/senlin/raw/master/images/屏幕截图 2026-07-24 151822.png) diff --git "a/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-28.md" "b/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-28.md" new file mode 100644 index 0000000000000000000000000000000000000000..d122c9262113f576e771086b63d1413d5eef3c4d --- /dev/null +++ "b/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-28.md" @@ -0,0 +1,39 @@ +# 日报 - 2026-07-28 + +**成员**:钟咏琪(全栈开发 ) + +## 今日完成 + +- NestJS 项目初始化: + ◦ 创建 tsconfig.json(target ES2022, strict: false, paths alias @/*) + ◦ 创建 nest-cli.json(sourceRoot: "src") + ◦ 创建 main.ts(setGlobalPrefix('api'), enableCors(), 监听 3000 端口) + ◦ 创建空 app.module.ts +- Prisma Schema 设计: + ◦ 创建 packages/shared/prisma/schema.prisma + ◦ 定义 User、Address、Cart、CartItem 四个模型 + ◦ 配置 @prisma/client 生成 +- 框架层搭建: + ◦ BusinessException 异常类 + ◦ DatabaseModule(PrismaClient provider) + ◦ RedisModule(RedisService: set/get/del) + ◦ RedisService(ioredis 连接,密码 redis123456) + +## 阻塞与求助 + +问题描述 +解决状态 +解决方式/求助对象 +Prisma 7 重大版本变更,datasource url 不再支持 +已解决 +配置 prisma.config.ts + @prisma/adapter-pg 适配器模式 + +## 明日计划 + +- 解决 Prisma 7 的连接问题(pg-pool + adapter-pg 版本兼容性) +- 完成用户模块的 DTO、Service、Controller 开发 +- 编写用户认证接口(注册、登录、获取当前用户) + +## 进度截图 + +![image-20260802212709597](https://gitee.com/zhongyongqi1121/senlin/raw/master/images/image-20260802212709597.png) \ No newline at end of file diff --git "a/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-29.md" "b/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-29.md" new file mode 100644 index 0000000000000000000000000000000000000000..628a43bc5d8bf387b1ba0a46aa9fb6b47b657532 --- /dev/null +++ "b/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-29.md" @@ -0,0 +1,39 @@ +# 日报 - 2026-07-29 + +**成员**:钟咏琪(全栈开发 ) + +## 今日完成 + +- 用户模块开发: + ◦ user.dto.ts:定义 RegisterDto、LoginAccountDto、LoginSmsDto、SendSmsDto、CreateAddressDto、UpdateAddressDto,使用 class-validator 装饰器 + ◦ user.service.ts:实现注册(bcrypt 密码加密)、账号登录、短信登录、获取当前用户、地址 CRUD 业务逻辑 + ◦ user.controller.ts:映射所有认证和地址接口路由 + ◦ user.module.ts:配置 JwtModule.registerAsync(),注册 UserController 和 UserService + ◦ jwt-auth.guard.ts:JWT 认证守卫,提取 Bearer Token 验证 +- 全局管道配置:在 main.ts 添加 ValidationPipe(transform: true) +- 环境变量处理:创建 .env 文件配置 DATABASE_URL + +## 阻塞与求助 + +问题描述 +解决状态 +解决方式/求助对象 +NestJS 无法注入 PrismaClient(token 不匹配) + 已解决 + 移除 @Inject('PRISMA_CLIENT'),直接使用类名作为 token +pg-pool@3.14.0 解析 connectionString 密码为 undefined + 已解决 + 手动正则解析 URL:postgresql://user:pass@host:port/db +.env 文件写入为 UTF-16 编码导致读取失败 + 已解决 + 使用 printf 重新创建为 UTF-8 格式 + +## 明日计划 + +- 修复 `JwtAuthGuard` 中 `req.user` 字段名问题(`userId` vs `sub`) +- 完善用户模块接口测试 +- 开发购物车模块 + +## 进度截图 + +![屏幕截图 2026-07-29 084830](https://gitee.com/zhongyongqi1121/senlin/raw/master/images/屏幕截图 2026-07-29 084830.png) \ No newline at end of file diff --git "a/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-30.md" "b/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-30.md" new file mode 100644 index 0000000000000000000000000000000000000000..b27a1092ef0b63831d480afb741da59a64721d51 --- /dev/null +++ "b/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-30.md" @@ -0,0 +1,43 @@ +# 日报 - 2026-07-30 + +**成员**:钟咏琪(全栈开发 ) + +## 今日完成 + +- 用户模块问题修复: + ◦ 修复 JwtAuthGuard 设置的 request.user 结构 + ◦ 修复 user.controller.ts 中所有 req.user.userId → req.user.sub + ◦ JWT payload 使用 sub 字段存储用户 ID +- 用户模块全量接口测试(全部通过): + ◦ ✅ 注册 POST /api/auth/register + ◦ ✅ 账号登录 POST /api/auth/login-account + ◦ ✅ 获取当前用户 GET /api/auth/me + ◦ ✅ 地址新增 POST /api/users/addresses + ◦ ✅ 地址列表 GET /api/users/addresses + ◦ ✅ 地址更新 PUT /api/users/addresses/:id + ◦ ✅ 设为默认 PATCH /api/users/addresses/:id/default + ◦ ✅ 删除地址 DELETE /api/users/addresses/:id +- 购物车模块开发启动: + ◦ 创建 cart.dto.ts:定义 AddCartItemDto、UpdateCartItemDto、SelectAllDto + +## 阻塞与求助 + +问题描述 +解决状态 +解决方式/求助对象 +JWT secret 变更导致旧 token 全部失效 + 已解决 + 重新登录获取新 token +PowerShell 中 curl 是 Invoke-WebRequest 别名导致 -H 参数报错 + 已解决 + 使用 Git Bash 执行 curl 命令避免别名冲突 + +## 明日计划 + +- 完成购物车模块:Service、Controller、Module +- 购物车接口全量测试 +- 编写模块 README.md 文档 + +## 进度截图 + +![image-20260802213423804](https://gitee.com/zhongyongqi1121/senlin/raw/master/images/image-20260802213423804.png) \ No newline at end of file diff --git "a/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-31.md" "b/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-31.md" new file mode 100644 index 0000000000000000000000000000000000000000..0e9a4731fcbbc08b7b5827d52c13f7975f4bdae7 --- /dev/null +++ "b/reports/daily/\351\222\237\345\222\217\347\220\252_2444310224/\346\227\245\346\212\245-2026-07-31.md" @@ -0,0 +1,42 @@ +# 日报 - 2026-07-31 + +**成员**:钟咏琪(全栈开发 ) + +## 今日完成 + +- 购物车模块开发完成: + ◦ cart.service.ts:实现 getCart()(自动创建购物车)、addItem()、updateItem()、deleteItem()、clearCart()、selectAll()、getSummary() 摘要计算 + ◦ cart.controller.ts:映射 7 个购物车接口,全部使用 @UseGuards(JwtAuthGuard) 鉴权 + ◦ cart.module.ts:注册 CartModule,导入 JwtModule(解决依赖注入问题) +- 购物车模块接口测试(全部通过): + ◦ ✅ 获取购物车 GET /api/cart + ◦ ✅ 添加商品 POST /api/cart/items + ◦ ✅ 更新数量 PATCH /api/cart/items/:id + ◦ ✅ 全选/全不选 POST /api/cart/items/select-all + ◦ ✅ 清空购物车 DELETE /api/cart/items + ◦ ✅ 获取摘要 GET /api/cart/summary +- 项目整合: + ◦ 更新 app.module.ts,导入 UserModule 和 CartModule + ◦ 验证用户模块 + 购物车模块共 15 个接口全部正常运行 + +## 阻塞与求助 + +问题描述 +解决状态 +解决方式/求助对象 +CartModule 引入 JwtAuthGuard 但无 JwtModule 依赖 + 已解决 + 在 CartModule 中单独导入 JwtModule.registerAsync() +PowerShell curl 别名导致 Token 在请求中被截断显示 ... + 已解决 + 在 Git Bash 中执行 curl 获取完整 token + +## 明日计划 + +- 编写用户模块 README.md 文档 +- 编写购物车模块 README.md 文档 +- Git 提交所有更改,规范化 commit 信息 + +## 进度截图 + +![image-20260802213610678](https://gitee.com/zhongyongqi1121/senlin/raw/master/images/image-20260802213610678.png) \ No newline at end of file