This commit is contained in:
27
src/App.tsx
Normal file
27
src/App.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { defineComponent, onMounted } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
|
||||
onMounted(() => {
|
||||
console.log("import.meta.env: ", import.meta.env)
|
||||
})
|
||||
|
||||
return () => (
|
||||
<>
|
||||
<div class="order-federation-placeholder">
|
||||
<h2>商品-联邦模块</h2>
|
||||
<p>此页面仅用于 商品模块 的单独开发和调试,可临时引入组件进行本地测试,实际运行时由 主应用
|
||||
加载本项目代码</p>
|
||||
|
||||
<h2>本模块包含的功能:</h2>
|
||||
<ul>
|
||||
<li>1:获取商品首页的数据和展示</li>
|
||||
<li>2:点击商品进入详情页</li>
|
||||
</ul>
|
||||
<p></p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
38
src/domains/product/di/productModule.ts
Normal file
38
src/domains/product/di/productModule.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// 从商品联邦模块导入领域层资源
|
||||
import {ProductRepository} from "@domains/product/ports/repositories/ProductRepository";
|
||||
import {HttpProductRepository} from "@domains/product/infr/api/HttpProductRepository";
|
||||
import {ProductApplicationService} from "@domains/product/application/services/ProductApplicationService";
|
||||
|
||||
/**
|
||||
* 商品领域的依赖注入配置
|
||||
* 将 接口 与 具体实现 绑定
|
||||
*
|
||||
* 优点:是一个依赖工厂,它隐藏了对象创建的细节,
|
||||
* 让业务代码只需要关心 “做什么”,而不用关心 “依赖从哪里来”
|
||||
* 所有依赖关系都在di目录下集中配置,一目了然,后续维护时能快速找到所有依赖的创建逻辑。
|
||||
*
|
||||
* 松耦合:
|
||||
* 业务代码(ProductApplicationService)只依赖抽象接口(ProductRepository),
|
||||
* 不依赖具体实现。如果未来需要改用其他数据来源(比如 WebSocket),
|
||||
* 只需修改bindProductRepository的返回值,无需改动业务逻辑。
|
||||
*
|
||||
* 易于测试:
|
||||
* 测试时可以临时替换为模拟实现
|
||||
* productModule.bindProductRepository = () => new MockProductRepository();
|
||||
* 这样测试就可以脱离真实 API,使用预设的模拟数据。
|
||||
*/
|
||||
export const product = {
|
||||
|
||||
// 绑定 仓储抽象接口 到 HTTP具体实现
|
||||
bindProductRepository: (): ProductRepository => {
|
||||
// return new MockProductRepository 直接从真实接口数据改为测试模拟数据
|
||||
return new HttpProductRepository;
|
||||
},
|
||||
|
||||
// 创建应用服务实例
|
||||
createProductApplicationService: (): ProductApplicationService => {
|
||||
return new ProductApplicationService(
|
||||
product.bindProductRepository()
|
||||
);
|
||||
}
|
||||
}
|
||||
141
src/domains/product/domain/entities/Product.ts
Normal file
141
src/domains/product/domain/entities/Product.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { ErrorText } from "../enums/ErrorEnums"
|
||||
|
||||
/**
|
||||
* 商品实体 - 包含商品的核心属性和行为
|
||||
* 在DDD中,实体具有唯一标识和生命周期
|
||||
*/
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
7
src/domains/product/domain/enums/ErrorEnums.ts
Normal file
7
src/domains/product/domain/enums/ErrorEnums.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export enum ErrorText {
|
||||
PriceNotNegativein = "商品 价格 不能为负数",
|
||||
StockNotNegativein = "商品 库存 不能为负数",
|
||||
StockIsGreaterThanZero = "减少的库存数量必须大于0",
|
||||
LowStock = "库存不足",
|
||||
IdNotNull = "商品Id不能为空"
|
||||
}
|
||||
1
src/domains/product/domain/events/README.md
Normal file
1
src/domains/product/domain/events/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||
9
src/domains/product/domain/type/ProductType.ts
Normal file
9
src/domains/product/domain/type/ProductType.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface ProductType {
|
||||
id: number,
|
||||
name: string,
|
||||
description: string,
|
||||
price: number,
|
||||
imageUrl: string,
|
||||
stock: number,
|
||||
categoryId: number,
|
||||
}
|
||||
1
src/domains/product/domain/value-objects/README.md
Normal file
1
src/domains/product/domain/value-objects/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||
6
src/domains/product/index.ts
Normal file
6
src/domains/product/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from './application/services/ProductApplicationService';
|
||||
export * from './domain/entities/Product';
|
||||
export * from './domain/enums/ErrorEnums';
|
||||
export * from './domain/type/ProductType';
|
||||
export * from './infr/api/HttpProductRepository';
|
||||
export * from './ports/repositories/ProductRepository';
|
||||
27
src/domains/product/infr/api/HttpProductRepository.ts
Normal file
27
src/domains/product/infr/api/HttpProductRepository.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Product } from "@domains/product/domain/entities/Product.ts";
|
||||
import { ProductType } from "@domains/product/domain/type/ProductType.ts";
|
||||
import { ProductRepository } from "@domains/product/ports/repositories/ProductRepository";
|
||||
import { http } from "shared/infra";
|
||||
import HttpConfig from "../config/HttpConfig";
|
||||
|
||||
|
||||
/**
|
||||
* HTTP商品仓储实现 - 实现商品数据的HTTP访问
|
||||
* 这是基础设施层,负责具体的数据获取实现
|
||||
*/
|
||||
export class HttpProductRepository implements ProductRepository {
|
||||
|
||||
async findAll(): Promise<Array<ProductType>> {
|
||||
const result: Array<ProductType> = await http.get({ url: HttpConfig.apiUrlList.productsListUrl });
|
||||
const items = result.map((item) => Product.fromDTO(item))
|
||||
return items;
|
||||
}
|
||||
|
||||
async findById(id: number): Promise<Product> {
|
||||
const result: ProductType = await http.get({
|
||||
url: HttpConfig.apiUrlList.productsInfoUrl,
|
||||
data: { id }
|
||||
});
|
||||
return Product.fromDTO(result);
|
||||
}
|
||||
}
|
||||
11
src/domains/product/infr/config/HttpConfig.ts
Normal file
11
src/domains/product/infr/config/HttpConfig.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
interface apiUrlListType {
|
||||
productsListUrl: string
|
||||
productsInfoUrl: string
|
||||
}
|
||||
|
||||
export default class HttpConfig {
|
||||
public static apiUrlList: apiUrlListType = {
|
||||
productsListUrl: '/productsList',
|
||||
productsInfoUrl: '/productsInfo',
|
||||
}
|
||||
}
|
||||
20
src/domains/product/ports/repositories/ProductRepository.ts
Normal file
20
src/domains/product/ports/repositories/ProductRepository.ts
Normal 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>
|
||||
}
|
||||
1
src/features/product/assets/README.md
Normal file
1
src/features/product/assets/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||
1
src/features/product/components/README.md
Normal file
1
src/features/product/components/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||
1
src/features/product/composables/README.md
Normal file
1
src/features/product/composables/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||
0
src/features/product/index.ts
Normal file
0
src/features/product/index.ts
Normal file
16
src/features/product/router/index.ts
Normal file
16
src/features/product/router/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
export const productRouter: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/Products',
|
||||
name: 'ProductList',
|
||||
component: () => import('../views/ProductList/ProductList.jsx'),
|
||||
meta: { title: '商品列表' }
|
||||
},
|
||||
{
|
||||
path: '/ProductDetail',
|
||||
name: 'ProductDetail',
|
||||
component: () => import('../views/ProductDetail/ProductDetail.jsx'),
|
||||
meta: { title: '商品详情' }
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
.ProductDetail {
|
||||
.title {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
62
src/features/product/views/ProductDetail/ProductDetail.tsx
Normal file
62
src/features/product/views/ProductDetail/ProductDetail.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import styles from './ProductDetail.module.less'
|
||||
|
||||
import { product } from '@domains/product/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({
|
||||
setup() {
|
||||
const route = useRoute();
|
||||
const productService = product.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={ styles.ProductDetail }>
|
||||
<h1 class={ styles.title }>商品领域模块 - 商品详情页面</h1>
|
||||
<hr/>
|
||||
<p>商品id:{ state.id }</p>
|
||||
{ state.item.name }
|
||||
<ul>
|
||||
{
|
||||
Object.entries(state.item).map(([ key, value ]) =>
|
||||
<li>{ key }: { value }</li>
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// export default defineComponent({
|
||||
// setup() {
|
||||
// return () => (
|
||||
// <div>商品详情</div>
|
||||
// )
|
||||
// }
|
||||
// })
|
||||
@@ -0,0 +1,5 @@
|
||||
.ProductList {
|
||||
.test {
|
||||
color: var(--theme-color);
|
||||
}
|
||||
}
|
||||
79
src/features/product/views/ProductList/ProductList.tsx
Normal file
79
src/features/product/views/ProductList/ProductList.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import styles from './ProductList.module.less'
|
||||
|
||||
import { defineComponent, onMounted, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router';
|
||||
import { product } from '@domains/product/di/productModule';
|
||||
import { ProductType } from '@domains/product/domain/type/ProductType';
|
||||
import { ElCard, ElIcon, Icons } from "shared/element-plus";
|
||||
|
||||
interface storeType {
|
||||
list: Array<ProductType>
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
|
||||
const router = useRouter();
|
||||
const productService = product.createProductApplicationService();
|
||||
|
||||
const state = reactive<storeType>({
|
||||
list: []
|
||||
})
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
loadProductList()
|
||||
})
|
||||
|
||||
const loadProductList = async () => {
|
||||
state.list = await productService.getProductList();
|
||||
console.log("state.list: ", state.list);
|
||||
}
|
||||
|
||||
const ElCardHeader = () => {
|
||||
return (
|
||||
<div class="card-header">
|
||||
<ElIcon size={ 15 }>
|
||||
<Icons.Position></Icons.Position>
|
||||
</ElIcon>
|
||||
<span>Card 头部</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ElCardFooter = () => {
|
||||
return <div>Footer 底部</div>
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div class={ styles.ProductList }>
|
||||
<h1 class={ styles.test }>商品领域模块 - 商品列表页面3</h1>
|
||||
<hr/>
|
||||
<h2>从Deno 后端 加载的数据,点击进详情</h2>
|
||||
<ElCard>
|
||||
{ {
|
||||
header: () => ElCardHeader(),
|
||||
footer: () => ElCardFooter(),
|
||||
default: () =>
|
||||
state.list.map(item =>
|
||||
<p key={ item.id } onClick={ () => {
|
||||
router.push({ name: 'ProductDetail', query: { id: item.id } });
|
||||
} }>
|
||||
{ item.id } | { item.name } | ¥{ item.price }
|
||||
</p>
|
||||
)
|
||||
} }
|
||||
</ElCard>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// export default defineComponent({
|
||||
// setup() {
|
||||
// return () => (
|
||||
// <div>商品列表</div>
|
||||
// )
|
||||
// }
|
||||
// })
|
||||
12
src/main.ts
Normal file
12
src/main.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App'
|
||||
|
||||
|
||||
createApp(App)
|
||||
.use(createPinia())
|
||||
.mount('#app')
|
||||
|
||||
4
src/shared/types/env.d.ts
vendored
Normal file
4
src/shared/types/env.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module '*.module.less' {
|
||||
const classes: { [key: string]: string }
|
||||
export default classes
|
||||
}
|
||||
29
src/shared/types/shared.d.ts
vendored
Normal file
29
src/shared/types/shared.d.ts
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module 'shared/infra' {
|
||||
export const http: any;
|
||||
}
|
||||
|
||||
declare module 'shared/utils' {
|
||||
export const UrlCheckUtils: any;
|
||||
}
|
||||
|
||||
declare module 'shared/element-plus' {
|
||||
import * as ElementPlus from 'element-plus';
|
||||
import * as ElementPlusIcons from '@element-plus/icons-vue';
|
||||
import type { App } from 'vue';
|
||||
export * from 'element-plus';
|
||||
export import Icons = ElementPlusIcons;
|
||||
|
||||
export function init(app: App): void;
|
||||
|
||||
const defaultExport: typeof ElementPlus & {
|
||||
Icons: typeof ElementPlusIcons;
|
||||
init: typeof init;
|
||||
};
|
||||
export default defaultExport;
|
||||
}
|
||||
|
||||
declare module 'shared/normalize' {
|
||||
export const init: () => void;
|
||||
}
|
||||
Reference in New Issue
Block a user