测试案例完成

This commit is contained in:
编码猿
2025-09-17 01:52:10 +08:00
parent 61c84b0842
commit 858018e43d
19 changed files with 431 additions and 22 deletions

View File

@@ -8,7 +8,8 @@ export default defineComponent({
<>
<div id="nav">
<RouterLink to="/"></RouterLink> |
<RouterLink to="/about"></RouterLink>
<RouterLink to="/about"></RouterLink> |
<RouterLink to="/Products"></RouterLink>
</div>
<RouterView></RouterView>
</>

View File

@@ -0,0 +1,38 @@
import { ProductApplicationService } from "../../domains/product/application/services/ProductApplicationService";
import { HttpProductRepository } from "../../domains/product/infr/api/HttpProductRepository";
import { ProductRepository } from "../../domains/product/ports/repositories/ProductRepository";
/**
* 商品领域的依赖注入配置
* 将 接口 与 具体实现 绑定
*
* 优点:是一个依赖工厂,它隐藏了对象创建的细节,
* 让业务代码只需要关心 “做什么”,而不用关心 “依赖从哪里来”
* 所有依赖关系都在di目录下集中配置一目了然后续维护时能快速找到所有依赖的创建逻辑。
*
* 松耦合:
* 业务代码ProductApplicationService只依赖抽象接口ProductRepository
* 不依赖具体实现。如果未来需要改用其他数据来源(比如 WebSocket
* 只需修改bindProductRepository的返回值无需改动业务逻辑。
*
* 易于测试:
* 测试时可以临时替换为模拟实现
* productModule.bindProductRepository = () => new MockProductRepository();
* 这样测试就可以脱离真实 API使用预设的模拟数据。
*/
export const productModule = {
// 绑定 仓储抽象接口 到 HTTP具体实现
bindProductRepository: (): ProductRepository => {
// return new MockProductRepository 直接从真实接口数据改为测试模拟数据
return new HttpProductRepository;
},
// 创建应用服务实例
createProductApplicationService: (): ProductApplicationService => {
return new ProductApplicationService(
productModule.bindProductRepository()
);
}
}

View File

@@ -1,24 +1,30 @@
import { createRouter, createWebHistory } from 'vue-router'
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { productRouter } from '@features/product/router'
const baseRouter: Array<RouteRecordRaw> = [
{
path: '/',
name: 'home',
component: () => import('../views/Home'),
},
{
path: '/about',
name: 'about',
component: () => import('../views/About'),
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('../views/NotFound')
}
];
// 聚合所有功能模块的路由
const routes: RouteRecordRaw[] = [...baseRouter, ...productRouter];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: () => import('../views/Home'),
},
{
path: '/about',
name: 'about',
component: () => import('../views/About'),
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('../views/NotFound')
}
],
routes,
})
export default router

View File

@@ -0,0 +1,18 @@
import { ErrorText } from "../../domain/enums/ErrorEnums";
import { ProductRepository } from "../../ports/repositories/ProductRepository";
export class ProductApplicationService {
// 依赖注入仓储接口,而不是具体实现
constructor(private productRepository: ProductRepository) { }
async getProductList() {
return await this.productRepository.findAll();
}
async getProductInfo(id: number) {
if (!id) {
throw new Error(ErrorText.IdNotNull)
}
return await this.productRepository.findById(id);
}
}

View File

@@ -0,0 +1,137 @@
import { ErrorText } from "../enums/ErrorEnums"
export class Product {
private readonly _id: number;
private _name: string;
private _description: string;
private _price: number;
private _imageUrl: string;
private _stock: number;
private _categoryId: number;
constructor(
id: number,
name: string,
description: string,
price: number,
imageUrl: string,
stock: number,
categoryId: number,
) {
this._id = id;
this._name = name;
this._description = description;
this._price = price;
this._imageUrl = imageUrl;
this._stock = stock;
this._categoryId = categoryId;
}
// 实体唯一标识
get id(): number {
return this._id;
}
public get name(): string {
return this._name;
}
public set name(value: string) {
this._name = value;
}
public get description(): string {
return this._description;
}
public set description(value: string) {
this._description = value;
}
public get price(): number {
return this._price;
}
public set price(value: number) {
if (value < 0) {
throw new Error(ErrorText.PriceNotNegativein);
}
this._price = value;
}
public get imageUrl(): string {
return this._imageUrl;
}
public set imageUrl(value: string) {
this._imageUrl = value;
}
public get stock(): number {
return this._stock;
}
public set stock(value: number) {
if (value < 0) {
throw new Error(ErrorText.StockNotNegativein)
}
this._stock = value;
}
public get categoryId(): number {
return this._categoryId;
}
public set categoryId(value: number) {
this._categoryId = value;
}
/**
* 检查商品是否有库存
*/
hasStock() {
return this._stock > 0
}
/**
* 减少商品库存
* @param quantity 减少的数量
*/
reduceStock(quantity: number): void {
if (quantity <= 0) {
throw new Error(ErrorText.StockIsGreaterThanZero)
}
if (this._stock < quantity) {
throw new Error(ErrorText.LowStock)
}
this._stock -= quantity;
}
/**
* Class类转化为普通json数据
*/
toDTO(): Record<string, string | number> {
return {
id: this._id,
name: this._name,
description: this._description,
price: this._price,
imageUrl: this._imageUrl,
stock: this._stock,
categoryId: this._categoryId
}
}
/**
* 从json数据创建Product类实例
* 转化为领域实体 Product
*/
static fromDTO(data: Record<string, any>): Product {
return new Product(
data.id,
data.name,
data.description,
data.price,
data.imageUrl,
data.stock,
data.categoryId,
);
}
}

View File

@@ -0,0 +1,7 @@
export enum ErrorText {
PriceNotNegativein = "商品 价格 不能为负数",
StockNotNegativein = "商品 库存 不能为负数",
StockIsGreaterThanZero = "减少的库存数量必须大于0",
LowStock = "库存不足",
IdNotNull = "商品Id不能为空"
}

View File

@@ -0,0 +1,9 @@
export interface ProductType {
id: number,
name: string,
description: string,
price: number,
imageUrl: string,
stock: number,
categoryId: number,
}

View File

@@ -0,0 +1,26 @@
import { Product } from "../../domain/entities/Product";
import { ProductType } from "../../domain/type/ProductType";
import { ProductRepository } from "../../ports/repositories/ProductRepository";
import http from "@shared/infra/AxiosInfra";
/**
* HTTP商品仓储实现 - 实现商品数据的HTTP访问
* 这是基础设施层,负责具体的数据获取实现
*/
export class HttpProductRepository implements ProductRepository {
async findAll(): Promise<Array<ProductType>> {
const result: Array<ProductType> = await http.get({ url: "/productsList" });
const items = result.map((item) => Product.fromDTO(item))
return items;
}
async findById(id: number): Promise<Product> {
const result: ProductType = await http.get({
url: "/productsInfo",
data: { id }
});
return Product.fromDTO(result);
}
}

View File

@@ -0,0 +1,20 @@
import { Product } from "../../domain/entities/Product"
import { ProductType } from "../../domain/type/ProductType"
/**
* 商品仓储接口 - 定义商品数据访问的契约
* 在DDD中端口定义了领域层与外部世界的交互方式
*/
export interface ProductRepository {
/**
* 获取商品列表
*/
findAll(): Promise<Array<ProductType>>;
/**
* 根据ID获取商品详情
* @param id 商品ID
*/
findById(id: number): Promise<Product>
}

View File

@@ -0,0 +1,17 @@
import { RouteRecordRaw } from 'vue-router';
export const productRouter: Array<RouteRecordRaw> = [
{
path: '/Products',
name: 'ProductList',
component: () => import('../views/ProductList.tsx'),
meta: { title: '商品列表' }
},
{
path: '/ProductDetail',
name: 'ProductDetail',
component: () => import('../views/ProductDetail.tsx'),
meta: { title: '商品详情' }
},
]

View File

@@ -0,0 +1,53 @@
import { productModule } from '@/di/productModule';
import { ProductType } from '@domains/product/domain/type/ProductType';
import { defineComponent, onMounted, reactive } from 'vue'
import { useRoute } from 'vue-router'
interface storeType {
item: ProductType
id: number
}
export default defineComponent({
name: "productModule-ProductDetail",
setup() {
const route = useRoute();
const productService = productModule.createProductApplicationService();
const state = reactive<storeType>({
item: {} as ProductType,
id: 0
})
onMounted(() => {
if (!route.query.id) {
throw new Error("缺少id参数");
}
state.id = Number(route.query.id)
console.log("id: ", state.id);
loadProductDetail()
})
const loadProductDetail = async () => {
state.item = await productService.getProductInfo(state.id);
console.log("state.item: ", state.item);
}
return () => (
<div class="ProductDetail">
<h1> - </h1>
<hr />
<p>id{state.id}</p>
{state.item.name}
<ul>
{
Object.entries(state.item).map(([key, value]) =>
<li>{key}: {value}</li>
)
}
</ul>
</div>
)
}
})

View File

@@ -0,0 +1,53 @@
import { defineComponent, onMounted, reactive } from 'vue'
import { useRouter } from 'vue-router';
import { productModule } from '@/di/productModule';
import { ProductType } from '@domains/product/domain/type/ProductType';
interface storeType {
list: Array<ProductType>
}
export default defineComponent({
name: "productModule-ProductList",
setup() {
const router = useRouter();
const productService = productModule.createProductApplicationService();
const state = reactive<storeType>({
list: []
})
onMounted(() => {
loadProductList()
})
const loadProductList = async () => {
state.list = await productService.getProductList();
console.log("state.list: ", state.list);
}
return () => (
<div class="ProductList">
<h1> - </h1>
<hr />
<h2>Deno </h2>
<ul>
{
state.list.map(item =>
<li key={item.id} onClick={() => {
router.push({
name: 'ProductDetail',
query: { id: item.id }
});
}}>
{item.id} | {item.name} | ¥{item.price}
</li>
)
}
</ul>
</div>
)
}
})