forked from yoone/WEB
1
0
Fork 0

Compare commits

...

No commits in common. "9495895ff1cb598c4030034f944333e7190a9d4f" and "f2f72f8c44e85db3eca88d84299a535c1bcdaee6" have entirely different histories.

176 changed files with 21041 additions and 15094 deletions

View File

@ -1,3 +0,0 @@
module.exports = {
extends: require.resolve('@umijs/max/eslint'),
};

7
.eslintrc.json Normal file
View File

@ -0,0 +1,7 @@
{
"extends": "./node_modules/mwts/",
"ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings"],
"env": {
"jest": true
}
}

34
.gitignore vendored
View File

@ -1,19 +1,17 @@
/node_modules logs/
/.env.local npm-debug.log
/.umirc.local.ts yarn-error.log
/config/config.local.ts node_modules/
/src/.umi coverage/
/src/.umi-production dist/
/src/.umi-test .idea/
/.umi run/
/.umi-production
/.umi-test
/dist
/.mfsu
.swc
/package-lock.json
/yarn.lock
*.yaml
#
/docs
.DS_Store .DS_Store
*.sw*
*.un~
.tsbuildinfo
.tsbuildinfo.*
yarn.lock
**/config.prod.ts
**/config.local.ts
container

2
.npmrc
View File

@ -1,2 +0,0 @@
registry=https://registry.npmmirror.com/

View File

@ -1,4 +0,0 @@
export default {
schemaPath: 'http://127.0.0.1:7001/swagger-ui/index.json',
serversPath: './src/servers',
};

View File

@ -1,3 +0,0 @@
node_modules
.umi
.umi-production

View File

@ -1,8 +0,0 @@
{
"printWidth": 80,
"singleQuote": true,
"trailingComma": "all",
"proseWrap": "never",
"overrides": [{ "files": ".prettierrc", "options": { "parser": "json" } }],
"plugins": ["prettier-plugin-organize-imports", "prettier-plugin-packagejson"]
}

3
.prettierrc.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
...require('mwts/.prettierrc.json')
}

View File

@ -1,3 +0,0 @@
module.exports = {
extends: require.resolve('@umijs/max/stylelint'),
};

234
.umirc.ts
View File

@ -1,234 +0,0 @@
import { defineConfig } from '@umijs/max';
const isDev = process.env.NODE_ENV === 'development';
const UMI_APP_API_URL = isDev
? 'http://localhost:7001'
: 'https://api.yoone.ca';
import { codeInspectorPlugin } from 'code-inspector-plugin';
export default defineConfig({
hash: true,
antd: {},
access: {},
model: {},
initialState: {},
request: {},
layout: {
title: 'YOONE',
},
define: {
UMI_APP_API_URL,
},
chainWebpack(config) {
config.plugin('code-inspector-plugin').use(
codeInspectorPlugin({
bundler: 'webpack',
})
);
},
routes: [
{ path: '/', redirect: '/home' },
{ name: '追踪', path: '/track', component: './Track', layout: false },
{ name: '登录', path: '/login', component: './Login', layout: false },
{ name: '首页', path: '/home', component: './Home' },
{
name: '组织架构',
path: '/organiza',
access: 'canSeeOrganiza',
routes: [
{
name: '用户管理',
path: '/organiza/user',
component: './Organiza/User',
},
],
},
{
name: '站点管理',
path: '/site',
access: 'canSeeSite',
routes: [
{
name: '站点列表',
path: '/site/list',
component: './Site/List',
},
],
},
{
name: '商品管理',
path: '/product',
access: 'canSeeProduct',
routes: [
{
name: '商品分类',
path: '/product/category',
component: './Product/Category',
},
{
name: '强度',
path: '/product/strength',
component: './Product/Strength',
},
{
name: '口味',
path: '/product/flavors',
component: './Product/Flavors',
},
{
name: '产品列表',
path: '/product/list',
component: './Product/List',
},
{
name: 'WP商品列表',
path: '/product/wp_list',
component: './Product/WpList',
},
],
},
{
name: '库存管理',
path: '/stock',
access: 'canSeeStock',
routes: [
{
name: '库存列表',
path: '/stock/list',
component: './Stock/List',
},
{
name: '仓库点',
path: '/stock/warehouse',
component: './Stock/Warehouse',
},
{
name: '采购管理',
path: '/stock/purchaseOrder',
component: './Stock/PurchaseOrder',
},
{
name: '发货管理',
path: '/stock/transfer',
component: './Stock/Transfer',
},
{
name: '库存记录',
path: '/stock/record',
component: './Stock/Record',
},
],
},
{
name: '订单管理',
path: '/order',
access: 'canSeeOrder',
routes: [
{
name: '订单列表',
path: '/order/list',
component: './Order/List',
},
{
name: '待发货产品',
path: '/order/item',
component: './Order/PendingItems',
},
],
},
// 新增:订阅管理路由分组(权限复用 canSeeOrder)
{
name: '订阅管理',
path: '/subscription',
access: 'canSeeOrder',
routes: [
{
name: '订阅列表',
path: '/subscription/list',
component: './Subscription/List',
},
{
name: '订单管理',
path: '/subscription/orders',
component: './Subscription/Orders',
},
],
},
{
name: '客户管理',
path: '/customer',
access: 'canSeeCustomer',
routes: [
{
name: '客户列表',
path: '/customer/list',
component: './Customer/List',
},
],
},
{
name: '物流管理',
path: '/logistics',
access: 'canSeeLogistics',
routes: [
{
name: '服务商',
path: '/logistics/services',
component: './Logistics/Services',
},
{
name: '地址管理',
path: '/logistics/address',
component: './Logistics/Address',
},
{
name: '物流列表',
path: '/logistics/list',
component: './Logistics/List',
},
],
},
{
name: '数据统计',
path: '/statistics',
access: 'canSeeStatistics',
routes: [
{
name: '销售统计',
path: '/statistics/sales',
component: './Statistics/Sales',
},
{
name: '订单统计',
path: '/statistics/order',
component: './Statistics/Order',
},
{
name: '订单来源',
path: '/statistics/orderSource',
component: './Statistics/OrderSource',
},
{
name: '客户统计',
path: '/statistics/customer',
component: './Statistics/Customer',
},
{
name: '库存预测',
path: '/statistics/inventoryForecast',
component: './Statistics/InventoryForecast',
},
{
name: '补货',
path: '/statistics/restocking',
component: './Statistics/Restocking',
},
],
},
// {
// path: '*',
// component: './404',
// },
],
npmClient: 'pnpm',
});

View File

@ -1,2 +1,3 @@
# WEB # API
中台api接口

2
bootstrap.js vendored Normal file
View File

@ -0,0 +1,2 @@
const { Bootstrap } = require('@midwayjs/bootstrap')
Bootstrap.run()

6
jest.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['<rootDir>/test/fixtures'],
coveragePathIgnorePatterns: ['<rootDir>/test/'],
};

View File

@ -1,20 +0,0 @@
const users = [
{ id: 0, name: 'Umi', nickName: 'U', gender: 'MALE' },
{ id: 1, name: 'Fish', nickName: 'B', gender: 'FEMALE' },
];
export default {
'GET /api/v1/queryUserList': (req: any, res: any) => {
res.json({
success: true,
data: { list: users },
errorCode: 0,
});
},
'PUT /api/v1/user/': (req: any, res: any) => {
res.json({
success: true,
errorCode: 0,
});
},
};

4159
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,44 +1,56 @@
{ {
"name": "my-midway-project",
"version": "1.0.0",
"description": "",
"private": true, "private": true,
"author": "cll <931958862@qq.com>",
"scripts": {
"build": "max build",
"dev": "max dev",
"format": "prettier --cache --write .",
"postinstall": "max setup",
"openapi2ts": "openapi2ts",
"prepare": "husky",
"setup": "max setup",
"start": "npm run dev"
},
"dependencies": { "dependencies": {
"@ant-design/charts": "^2.2.6", "@midwayjs/bootstrap": "^3.20.0",
"@ant-design/icons": "^5.0.1", "@midwayjs/core": "^3.20.0",
"@ant-design/pro-components": "^2.4.4", "@midwayjs/cron": "^3.20.0",
"@fingerprintjs/fingerprintjs": "^4.6.2", "@midwayjs/cross-domain": "^3.20.2",
"@umijs/max": "^4.4.4", "@midwayjs/decorator": "^3.20.0",
"@umijs/max-plugin-openapi": "^2.0.3", "@midwayjs/info": "^3.20.2",
"@umijs/plugin-openapi": "^1.3.3", "@midwayjs/jwt": "^3.20.2",
"antd": "^5.4.0", "@midwayjs/koa": "^3.20.2",
"dayjs": "^1.11.9", "@midwayjs/logger": "^3.1.0",
"echarts": "^5.6.0", "@midwayjs/swagger": "^3.20.2",
"echarts-for-react": "^3.0.2", "@midwayjs/typeorm": "^3.20.0",
"file-saver": "^2.0.5", "@midwayjs/validate": "^3.20.2",
"print-js": "^1.6.0", "@woocommerce/woocommerce-rest-api": "^1.0.2",
"react-phone-input-2": "^2.15.1", "axios": "^1.7.9",
"react-toastify": "^11.0.5", "bcryptjs": "^2.4.3",
"xlsx": "^0.18.5" "class-transformer": "^0.5.1",
"dayjs": "^1.11.13",
"mysql2": "^3.11.5",
"nodemailer": "^7.0.5",
"swagger-ui-dist": "^5.18.2",
"typeorm": "^0.3.20",
"xml2js": "^0.6.2"
}, },
"engines": {
"node": ">=12.0.0"
},
"scripts": {
"start": "cross-env NODE_ENV=prod pm2 start ./bootstrap.js --name yoone -i 4",
"prod": "cross-env NODE_ENV=prod node ./bootstrap.js ",
"dev": "cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/app.js",
"test": "cross-env NODE_ENV=unittest jest",
"cov": "jest --coverage",
"lint": "mwts check",
"lint:fix": "mwts fix",
"ci": "npm run cov",
"build": "mwtsc --cleanOutDir"
},
"repository": {
"type": "git",
"url": ""
},
"author": "anonymous",
"license": "MIT",
"devDependencies": { "devDependencies": {
"@types/react": "^18.0.33", "@midwayjs/mock": "^3.20.11",
"@types/react-dom": "^18.0.11", "cross-env": "^10.1.0",
"code-inspector-plugin": "^1.2.10", "mwtsc": "^1.15.2",
"husky": "^9", "typescript": "^5.9.3"
"lint-staged": "^13.2.0",
"openapi2ts": "^1.1.14",
"prettier": "^2.8.7",
"prettier-plugin-organize-imports": "^3.2.2",
"prettier-plugin-packagejson": "^2.4.3",
"typescript": "^5.7.3"
} }
} }

2687
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,24 +0,0 @@
export default (initialState: any) => {
const isSuper = initialState?.user?.isSuper ?? false;
const isAdmin = initialState?.user?.Admin ?? false;
const canSeeOrganiza = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('organiza') ?? false);
const canSeeProduct = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('product') ?? false);
const canSeeStock = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('stock') ?? false);
const canSeeOrder = (isSuper || isAdmin) ||
((initialState?.user?.permissions?.includes('order') ?? false) || (initialState?.user?.permissions?.includes('order-10-days') ?? false));
const canSeeCustomer = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('customer') ?? false);
const canSeeLogistics = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('logistics') ?? false);
const canSeeStatistics = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('statistics') ?? false);
const canSeeSite = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('site') ?? false);
return {
canSeeOrganiza,
canSeeProduct,
canSeeStock,
canSeeOrder,
canSeeCustomer,
canSeeLogistics,
canSeeStatistics,
canSeeSite,
};
};

View File

@ -1,128 +0,0 @@
// 运行时配置
import { LogoutOutlined, UserOutlined } from '@ant-design/icons';
import {
ProLayoutProps,
ProSchemaValueEnumObj,
ProTable,
} from '@ant-design/pro-components';
import { RequestConfig, history, useModel } from '@umijs/max';
import { App, Avatar, ConfigProvider, Dropdown, MenuProps } from 'antd';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import { usercontrollerGetuser } from './servers/api/user';
// 设置 dayjs 全局语言为中文
dayjs.locale('zh-cn');
// 全局初始化数据配置,用于 Layout 用户信息和权限初始化
// 更多信息见文档:https://umijs.org/docs/api/runtime-config#getinitialstate
export async function getInitialState(): Promise<{
user?: Record<string, any>;
categoryList?: ProSchemaValueEnumObj;
sites?: API.SiteConfig[];
}> {
if (!localStorage.getItem('token') || history.location.pathname === '/login')
return {};
const { data: user } = await usercontrollerGetuser();
return { user };
}
export const layout = (): ProLayoutProps => {
const { initialState } = useModel('@@initialState');
const items: MenuProps['items'] = [
{
key: '1',
label: '我的账号',
disabled: true,
},
{
type: 'divider',
},
{
key: '3',
label: '退出登录',
icon: <LogoutOutlined />,
onClick: async () => {
//TODO 清理服务器登陆状态
localStorage.removeItem('token');
history.push('/login');
},
},
];
return {
menu: {
locale: false,
},
layout: 'mix',
actionsRender: () => (
<Dropdown key="avatar" menu={{ items }}>
<div style={{ cursor: 'pointer' }}>
<Avatar size="large" icon={<UserOutlined />} />
<span style={{ marginLeft: 8 }}>{initialState?.name}</span>
</div>
</Dropdown>
),
};
};
export const request: RequestConfig = {
baseURL: UMI_APP_API_URL,
requestInterceptors: [
(url: string, options: any) => {
const token = localStorage.getItem('token');
return {
url,
options: {
...options,
headers: {
...options.headers,
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
},
};
},
],
errorConfig: {
errorHandler: (error: any) => {
if (error?.response?.status === 401) {
localStorage.removeItem('token');
history.push('/login');
}
},
},
};
export const onRouteChange = ({ location }: { location: Location }) => {
const token = localStorage.getItem('token');
// 白名单,不需要登录的页面
const whiteList = ['/login', '/track'];
if (!token && !whiteList.includes(location.pathname)) {
// 没有 token 且不在白名单内,跳转到登录页
history.push('/login');
}
};
export function rootContainer(container: React.ReactNode) {
// 全局过滤空字段逻辑
(ProTable as any).defaultProps = {
...(ProTable as any).defaultProps,
beforeSearchSubmit: (params: any) => {
return Object.fromEntries(
Object.entries(params).filter(
([_, value]) => value !== undefined && value !== null && value !== '',
),
);
},
};
return (
<ConfigProvider>
<App>{container}</App>
</ConfigProvider>
);
}

View File

@ -1,84 +0,0 @@
import { Input, Select } from 'antd';
import React, { useEffect } from 'react';
const { Option } = Select;
// 定义国家代码和扩展码的映射关系
const phoneExtensions: { [key: string]: string } = {
CA: '1',
CN: '86',
US: '1',
};
interface InternationalPhoneInputProps {
value?: { country?: string; phone?: string; extension?: string };
onChange?: (value: {
country: string;
phone: string;
extension?: string;
}) => void;
}
const InternationalPhoneInput: React.FC<InternationalPhoneInputProps> = ({
value = {},
onChange,
}) => {
const {
country = 'CA',
phone = '',
extension = phoneExtensions['CA'],
} = value || {};
useEffect(() => {
triggerChange({ extension });
}, []);
const triggerChange = (
changedValue: Partial<{
country: string;
phone: string;
extension?: string;
}>,
) => {
if (onChange) {
onChange({
country,
phone,
...value,
...changedValue,
});
}
};
const onCountryChange = (newCountry: string) => {
triggerChange({
country: newCountry,
extension: phoneExtensions[newCountry],
});
};
const onPhoneChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
triggerChange({ phone: e.target.value });
};
return (
<Input
addonBefore={
<Select
value={country}
onChange={onCountryChange}
style={{ width: 100 }}
>
<Option value="CA">🇨🇦 +1</Option>
<Option value="CN">🇨🇳 +86</Option>
<Option value="US">🇺🇸 +1</Option>
{/* 添加更多国家代码 */}
</Select>
}
value={phone}
onChange={onPhoneChange}
placeholder="请输入联系电话"
/>
);
};
export default InternationalPhoneInput;

View File

@ -0,0 +1,132 @@
import { MidwayConfig } from '@midwayjs/core';
import { Product } from '../entity/product.entity';
import { Category } from '../entity/category.entity';
import { WpProduct } from '../entity/wp_product.entity';
import { Variation } from '../entity/variation.entity';
import { User } from '../entity/user.entity';
import { PurchaseOrder } from '../entity/purchase_order.entity';
import { PurchaseOrderItem } from '../entity/purchase_order_item.entity';
import { Stock } from '../entity/stock.entity';
import { StockPoint } from '../entity/stock_point.entity';
import { StockRecord } from '../entity/stock_record.entity';
import { Order } from '../entity/order.entity';
import { OrderItem } from '../entity/order_item.entity';
import { OrderCoupon } from '../entity/order_copon.entity';
import { OrderFee } from '../entity/order_fee.entity';
import { OrderRefund } from '../entity/order_refund.entity';
import { OrderRefundItem } from '../entity/order_retund_item.entity';
import { OrderSale } from '../entity/order_sale.entity';
import { OrderSaleOriginal } from '../entity/order_item_original.entity';
import { OrderShipping } from '../entity/order_shipping.entity';
import { Service } from '../entity/service.entity';
import { ShippingAddress } from '../entity/shipping_address.entity';
import { OrderNote } from '../entity/order_note.entity';
import { OrderShipment } from '../entity/order_shipment.entity';
import { Shipment } from '../entity/shipment.entity';
import { ShipmentItem } from '../entity/shipment_item.entity';
import { Transfer } from '../entity/transfer.entity';
import { TransferItem } from '../entity/transfer_item.entity';
import { Strength } from '../entity/strength.entity';
import { Flavors } from '../entity/flavors.entity';
import { CustomerTag } from '../entity/customer_tag.entity';
import { Customer } from '../entity/customer.entity';
import { DeviceWhitelist } from '../entity/device_whitelist';
import { AuthCode } from '../entity/auth_code';
import { Subscription } from '../entity/subscription.entity';
import { Site } from '../entity/site.entity';
export default {
// use for cookie sign key, should change to your own and keep security
keys: '1733728588817_720',
typeorm: {
default: {
entities: [
Product,
Category,
Strength,
Flavors,
WpProduct,
Variation,
User,
PurchaseOrder,
PurchaseOrderItem,
Stock,
StockPoint,
StockRecord,
Order,
OrderItem,
OrderCoupon,
OrderFee,
OrderRefund,
OrderRefundItem,
OrderSale,
OrderSaleOriginal,
OrderShipment,
ShipmentItem,
Shipment,
OrderShipping,
Service,
ShippingAddress,
OrderNote,
Transfer,
TransferItem,
CustomerTag,
Customer,
DeviceWhitelist,
AuthCode,
Subscription,
Site,
],
synchronize: true,
logging: false,
},
dataSource: {
default: {
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'inventory',
},
},
},
// cors: {
// origin: '*', // 允许所有来源跨域请求
// allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法
// allowHeaders: ['Content-Type', 'Authorization'], // 允许的自定义请求头
// credentials: true, // 允许携带凭据cookies等
// },
// jwt: {
// secret: 'YOONE2024!@abc',
// expiresIn: '7d',
// },
// wpSite: [
// {
// id: '2',
// wpApiUrl: 'http://localhost:10004',
// consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652',
// consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38',
// siteName: 'Local',
// email: 'tom@yoonevape.com',
// emailPswd: '',
// },
// ],
swagger: {
auth: {
name: 'authorization',
authType: 'bearer',
description: 'Bearer Auth',
addSecurityRequirements: true,
},
},
mailer: {
host: 'smtphz.qiye.163.com',
port: 465,
secure: true,
auth: {
user: 'info@canpouches.com',
pass: 'WWqQ4aZq4Jrm9uwz',
},
}
} as MidwayConfig;

View File

@ -0,0 +1,86 @@
import { MidwayConfig } from '@midwayjs/core';
export default {
koa: {
port: 7001,
},
// typeorm: {
// dataSource: {
// default: {
// host: '13.212.62.127',
// username: 'root',
// password: 'Yoone!@.2025',
// },
// },
// },
typeorm: {
dataSource: {
default: {
host: 'localhost',
username: 'root',
password: '12345678',
},
},
},
cors: {
origin: '*', // 允许所有来源跨域请求
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法
allowHeaders: ['Content-Type', 'Authorization'], // 允许的自定义请求头
credentials: true, // 允许携带凭据cookies等
},
jwt: {
secret: 'YOONE2024!@abc',
expiresIn: '7d',
},
wpSite: [
{
id: '-1',
siteName: 'Admin',
email: '2469687281@qq.com',
},
{
id: '2',
wpApiUrl: 'http://t2-shop.local/',
consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742',
consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c',
siteName: 'Local',
email: '2469687281@qq.com',
emailPswd: 'lulin91.',
},
{
id: '3',
wpApiUrl: 'http://t1-shop.local/',
consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742',
consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c',
siteName: 'Local-test-2',
email: '2469687281@qq.com',
emailPswd: 'lulin91.',
},
// {
// id: '2',
// wpApiUrl: 'http://localhost:10004',
// consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652',
// consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38',
// siteName: 'Local',
// email: 'tom@yoonevape.com',
// emailPswd: 'lulin91.',
// },
],
freightcom: {
url: 'https://customer-external-api.ssd-test.freightcom.com',
token: '6zGj1qPTL1jIkbLmgaiYc6SwHUIXJ2t25htUF8uuFYiCg8ILCY6xnBEbvrX1p79L',
},
canadaPost: {
url: 'https://ct.soa-gw.canadapost.ca',
username: '65d23d3a75d7baf7',
password: '56443bb98b68dfdd60f52e',
customerNumber: '0006122480',
contractId: '0044168528',
},
uniExpress: {
url: 'https://sjqa.uniexpress.org', // 测试环境url
// url: 'https://sj.uniexpress.ca', //正式环境url
clientId: '101018',
clientSecret: 'cbcb51bea204f3f69c47b5280064408e',
customerNo: 2067,
}
} as MidwayConfig;

View File

@ -0,0 +1,7 @@
import { MidwayConfig } from '@midwayjs/core';
export default {
koa: {
port: null,
},
} as MidwayConfig;

82
src/configuration.ts Normal file
View File

@ -0,0 +1,82 @@
import {
Configuration,
App,
Inject,
MidwayDecoratorService,
} from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import * as validate from '@midwayjs/validate';
import * as info from '@midwayjs/info';
import * as orm from '@midwayjs/typeorm';
import { join } from 'path';
// import { DefaultErrorFilter } from './filter/default.filter';
// import { NotFoundFilter } from './filter/notfound.filter';
import { ReportMiddleware } from './middleware/report.middleware';
import * as swagger from '@midwayjs/swagger';
import * as crossDomain from '@midwayjs/cross-domain';
import * as cron from '@midwayjs/cron';
import * as jwt from '@midwayjs/jwt';
import { USER_KEY } from './decorator/user.decorator';
import { SiteService } from './service/site.service';
import { AuthMiddleware } from './middleware/auth.middleware';
@Configuration({
imports: [
koa,
validate,
{
component: info,
enabledEnvironment: ['local', 'prod'],
},
orm,
swagger,
crossDomain,
cron,
jwt,
],
importConfigs: [join(__dirname, './config')],
})
export class MainConfiguration {
@App('koa')
app: koa.Application;
@Inject()
decoratorService: MidwayDecoratorService;
@Inject()
jwtService: jwt.JwtService; // 注入 JwtService 实例
@Inject()
siteService: SiteService;
async onReady() {
// add middleware
this.app.useMiddleware([ReportMiddleware, AuthMiddleware]);
// add filter
// this.app.useFilter([NotFoundFilter, DefaultErrorFilter]);
this.decoratorService.registerParameterHandler(
USER_KEY,
async (
options
): Promise<{
id: number;
userName: string;
}> => {
const ctx = options.originArgs[0];
const token = ctx.headers['authorization']?.split(' ')[1];
const config = ctx.app.getConfig('jwt'); // 动态获取配置项
if (!token) {
ctx.throw(401, 'Token not found');
}
try {
const decoded: any = this.jwtService.verify(token, config.secret); // 替换为你的密钥
return decoded;
} catch (error) {
ctx.throw(401, 'Invalid token');
}
}
);
}
}

View File

@ -1,120 +0,0 @@
import { ProSchemaValueEnumObj } from '@ant-design/pro-components';
export const DEFAULT_NAME = 'YOONE';
export const PRODUCT_STATUS_ENUM: ProSchemaValueEnumObj = {
publish: {
text: '已发布',
status: 'success',
},
draft: {
text: '草稿',
status: 'draft',
},
pending: {
text: '待审核',
status: 'pending',
},
private: {
text: '私有',
status: 'private',
},
trash: {
text: '已删除',
status: 'error',
},
'auto-draft': {
text: '自动草稿',
status: 'auto-draft',
},
future: {
text: '定时发布',
status: 'success',
},
inherit: {
text: '继承状态',
status: 'inherit',
},
};
export const PRODUCT_STOCK_STATUS_ENUM: ProSchemaValueEnumObj = {
instock: {
text: '上架',
status: 'instock',
},
outofstock: {
text: 'out of stock',
status: 'outofstock',
},
onbackorder: {
text: 'on back order',
status: 'onbackorder',
},
};
export const Purchase_Order_STATUS_ENUM: ProSchemaValueEnumObj = {
draft: {
text: '草稿',
status: 'default',
},
submitted: {
text: '已发货',
status: 'warning',
},
received: {
text: '已到达',
status: 'success',
disabled: true,
},
};
export const ORDER_STATUS_ENUM: ProSchemaValueEnumObj = {
pending: {
text: '待确认',
status: 'default',
},
processing: {
text: '待发货',
status: 'success',
},
completed: {
text: '已完成',
status: 'success',
},
cancelled: {
text: '已取消',
status: 'error',
},
refunded: {
text: '已退款',
status: 'warning',
},
failed: {
text: '失败',
status: 'error',
},
after_sale_pending: {
text: '售后处理中',
status: 'warning',
},
pending_reshipment: {
text: '待补发',
status: 'warning',
},
pending_refund: {
text: '待退款',
status: 'warning',
},
refund_requested: {
text: '已申请退款',
status: 'refund_approved',
},
refund_approved: {
text: '退款申请已通过',
status: 'refund_approved',
},
refund_cancelled: {
text: '已取消退款',
status: 'refund_cancelled',
}
};

View File

@ -0,0 +1,9 @@
import { Inject, Controller } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
@Controller('/')
export class APIController {
@Inject()
ctx: Context;
}

View File

@ -0,0 +1,82 @@
import {
Body,
Context,
Controller,
Del,
Get,
Inject,
Post,
Put,
Query,
} from '@midwayjs/core';
import { CustomerService } from '../service/customer.service';
import { errorResponse, successResponse } from '../utils/response.util';
import { ApiOkResponse } from '@midwayjs/swagger';
import { BooleanRes } from '../dto/reponse.dto';
import { CustomerTagDTO, QueryCustomerListDTO } from '../dto/customer.dto';
@Controller('/customer')
export class CustomerController {
@Inject()
ctx: Context;
@Inject()
customerService: CustomerService;
@ApiOkResponse()
@Get('/list')
async getCustomerList(@Query() param: QueryCustomerListDTO) {
try {
const data = await this.customerService.getCustomerList(param);
return successResponse(data);
} catch (error) {
console.log(error)
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({ type: BooleanRes })
@Post('/tag/add')
async addTag(@Body() dto: CustomerTagDTO) {
try {
await this.customerService.addTag(dto.email, dto.tag);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({ type: BooleanRes })
@Del('/tag/del')
async delTag(@Body() dto: CustomerTagDTO) {
try {
await this.customerService.delTag(dto.email, dto.tag);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Get('/tags')
async getTags() {
try {
const data = await this.customerService.getTags();
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({ type: BooleanRes })
@Put('/rate')
async setRate(@Body() params: { id: number; rate: number }) {
try {
await this.customerService.setRate(params);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || error);
}
}
}

View File

@ -0,0 +1,288 @@
import {
Inject,
Controller,
Post,
Body,
Get,
Put,
Param,
Del,
Query,
} from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
import { ApiOkResponse } from '@midwayjs/swagger';
import {
BooleanRes,
RateLitRes,
ServiceListRes,
ShippingAddressListRes,
} from '../dto/reponse.dto';
import { FreightcomService } from '../service/freightcom.service';
import { errorResponse, successResponse } from '../utils/response.util';
import { LogisticsService } from '../service/logistics.service';
import { ShippingDetailsDTO } from '../dto/freightcom.dto';
import { ShippingAddress } from '../entity/shipping_address.entity';
import { QueryServiceDTO, ShipmentBookDTO, ShipmentFeeBookDTO } from '../dto/logistics.dto';
import { User } from '../decorator/user.decorator';
@Controller('/logistics')
export class LogisticsController {
@Inject()
ctx: Context;
@Inject()
freightcomService: FreightcomService;
@Inject()
logisticsService: LogisticsService;
@ApiOkResponse({
type: BooleanRes,
})
@Post('/syncServices')
async syncServices() {
try {
await this.freightcomService.syncServices();
return successResponse(true);
} catch (error) {
return errorResponse('同步失败');
}
}
@ApiOkResponse({
description: '服务商列表',
type: ServiceListRes,
})
@Get('/getServiceList')
async getServiceList(
@Query()
param: QueryServiceDTO
) {
try {
const data = await this.logisticsService.getServiceList(param);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse()
@Post('/toggleActive')
async toggleActive(@Body() body: { id: string; isActive: boolean }) {
try {
await this.logisticsService.toggleServiceActive(body.id, body.isActive);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse({
type: RateLitRes,
})
@Post('/getRateList')
async getRateList(@Body() details: ShippingDetailsDTO) {
try {
const rates = await this.logisticsService.getRateList(details);
return successResponse(rates);
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse({
type: BooleanRes,
})
@Post('/createShippingAddress')
async createShippingAddress(@Body() shippingAddress: ShippingAddress) {
try {
await this.logisticsService.createShippingAddress(shippingAddress);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || '创建失败');
}
}
@ApiOkResponse({
type: BooleanRes,
})
@Put('/updateShippingAddress/:id')
async updateShippingAddress(
@Body() shippingAddress: ShippingAddress,
@Param('id') id: number
) {
try {
await this.logisticsService.updateShippingAddress(id, shippingAddress);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || '更新失败');
}
}
@ApiOkResponse({
type: ShippingAddressListRes,
})
@Get('/getShippingAddressList')
async getShippingAddressList() {
try {
const data = await this.logisticsService.getShippingAddressList();
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse({
type: BooleanRes,
})
@Del('/delShippingAddress/:id')
async delShippingAddress(@Param('id') id: number) {
try {
const boolen = await this.logisticsService.delShippingAddress(id);
return successResponse(boolen);
} catch (error) {
return errorResponse(error?.message || '删除失败');
}
}
@ApiOkResponse({
type: BooleanRes,
})
@Post('/deleteShipment/:id')
async deleteShipment(@Param('id') id: number) {
try {
const res = await this.logisticsService.removeShipment(id);
return successResponse(res);
} catch (error) {
return errorResponse(error?.message || '删除运单失败' );
}
}
@ApiOkResponse({
type: BooleanRes,
})
@Post('/createShipment/:orderId')
async createShipment(
@Param('orderId') orderId: number,
@Body() data: ShipmentBookDTO,
@User() user
) {
try {
const res: any = await this.logisticsService.createShipment(orderId, data, user.id);
return successResponse(res.data);
} catch (error) {
return errorResponse(error?.message || '创建失败');
}
}
@ApiOkResponse(
{type: BooleanRes}
)
@Post('/getShipmentFee')
async getShipmentFee(
@Body() data: ShipmentFeeBookDTO
) {
try {
const fee = await this.logisticsService.getShipmentFee(data);
return successResponse(fee);
} catch (error) {
return errorResponse(error?.message || '创建失败');
}
}
@ApiOkResponse(
{type: BooleanRes}
)
@Post('/getShipmentLabel/:shipmentId')
async getShipmentLabel(
@Param('shipmentId') shipmentId: number
) {
try {
const res = await this.logisticsService.getShipmentLabel(shipmentId);
if (res.data.data[0].status === 'Success') {
return successResponse({ content: res.data.data[0].labelContent });
} else {
return errorResponse(res.data.data[0].errors);
}
} catch (error) {
return errorResponse(error?.message || '创建失败');
}
}
@ApiOkResponse()
@Post('/getPaymentMethods')
async getpaymentmethods() {
try {
const data = await this.freightcomService.getPaymentMethods();
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse()
@Post('/updateState/:shipmentId')
async updateShipmentState(
@Param('shipmentId') shipmentId: number
) {
try {
const data = await this.logisticsService.updateShipmentStateById(shipmentId);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '更新运单状态失败')
}
}
@ApiOkResponse()
@Del('/shipment/:id')
async delShipment(@Param('id') id: number, @User() user) {
try {
const data = await this.logisticsService.delShipment(id, user.id);
return successResponse(data);
} catch (error) {
console.log(error);
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse()
@Post('/getOrderList')
async getOrderList(@Query('number') number: string) {
try {
return successResponse(
await this.logisticsService.getOrderList(number)
);
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse()
@Post('/getListByOrderId')
async getListByOrderId(@Query('id') orderId: number) {
try {
return successResponse(
await this.logisticsService.getListByOrderId(orderId)
);
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse({
description: '物流列表',
})
@Get('/list')
async getList(
@Query()
param: Record<string, any>
) {
try {
const data = await this.logisticsService.getList(param);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
}

View File

@ -0,0 +1,255 @@
import {
Body,
Controller,
Del,
Get,
Inject,
Param,
Post,
Put,
Query,
} from '@midwayjs/core';
import { ApiOkResponse } from '@midwayjs/swagger';
import {
BooleanRes,
OrderDetailRes,
OrderListRes,
OrderSaleListRes,
} from '../dto/reponse.dto';
import { OrderService } from '../service/order.service';
import { errorResponse, successResponse } from '../utils/response.util';
import {
CreateOrderNoteDTO,
QueryOrderDTO,
QueryOrderSalesDTO,
QueryOrderItemDTO,
} from '../dto/order.dto';
import { User } from '../decorator/user.decorator';
import { ErpOrderStatus } from '../enums/base.enum';
@Controller('/order')
export class OrderController {
@Inject()
orderService: OrderService;
@ApiOkResponse({
type: BooleanRes,
})
@Post('/syncOrder/:siteId')
async syncOrder(@Param('siteId') siteId: string) {
try {
await this.orderService.syncOrders(siteId);
return successResponse(true);
} catch (error) {
console.log(error);
return errorResponse('同步失败');
}
}
@ApiOkResponse({
type: BooleanRes,
})
@Post('/syncOrder/:siteId/order/:orderId')
async syncOrderById(
@Param('siteId') siteId: string,
@Param('orderId') orderId: string
) {
try {
await this.orderService.syncOrderById(siteId, orderId);
return successResponse(true);
} catch (error) {
console.log(error);
return errorResponse('同步失败');
}
}
@ApiOkResponse({
type: OrderListRes,
})
@Get('/getOrders')
async getOrders(
@Query()
param: QueryOrderDTO,
@User() user
) {
try {
const count = await this.orderService.getOrderStatus(param);
const data = await this.orderService.getOrders(param, user.id);
return successResponse({
...data,
count,
});
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse({
type: OrderSaleListRes,
})
@Get('/getOrderSales')
async getOrderSales(@Query() param: QueryOrderSalesDTO) {
try {
if (param.isSource)
return successResponse(await this.orderService.getOrderItems(param));
return successResponse(await this.orderService.getOrderSales(param));
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse()
@Get('/getOrderItems')
async getOrderItems(@Query() param: QueryOrderSalesDTO) {
try {
return successResponse(await this.orderService.getOrderItems(param));
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse()
@Get('/getOrderItemList')
async getOrderItemList(@Query() param: QueryOrderItemDTO) {
try {
return successResponse(await this.orderService.getOrderItemList(param));
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse({
type: OrderDetailRes,
})
@Get('/:orderId')
async getOrderDetail(@Param('orderId') orderId: number) {
try {
return successResponse(await this.orderService.getOrderDetail(orderId));
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse()
@Get('/:orderId/related')
async getRelatedByOrder(@Param('orderId') orderId: number) {
try {
return successResponse(await this.orderService.getRelatedByOrder(orderId));
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse({
type: BooleanRes,
})
@Post('/updateOrderItems/:orderId')
async updateOrderItems(
@Param('orderId') orderId: number,
@Body() data: any,
) {
try {
const res = await this.orderService.updateExchangeOrder(orderId, data);
return successResponse(res);
} catch (error) {
return errorResponse(error?.message || '更新失败');
}
}
@ApiOkResponse({
type: BooleanRes,
})
@Del('/:id')
async delOrder(@Param('id') id: number) {
try {
return successResponse(await this.orderService.delOrder(id));
} catch (error) {
return errorResponse(error?.message || '删除失败');
}
}
@ApiOkResponse({
type: BooleanRes,
})
@Post('/createNote')
async createNote(@Body() data: CreateOrderNoteDTO, @User() user) {
try {
return successResponse(await this.orderService.createNote(user.id, data));
} catch (error) {
return errorResponse(error?.message || '创建失败');
}
}
@ApiOkResponse()
@Post('/getOrderByNumber')
async getOrderByNumber(@Body('number') number: string) {
try {
return successResponse(await this.orderService.getOrderByNumber(number));
} catch (error) {
return errorResponse(error?.message || '创建失败');
}
}
@ApiOkResponse()
@Post('/order/cancel/:id')
async cancelOrder(@Param('id') id: number) {
try {
return successResponse(await this.orderService.cancelOrder(id));
} catch (error) {
return errorResponse(error?.message || '创建失败');
}
}
@ApiOkResponse()
@Post('/order/refund/:id')
async refundOrder(@Param('id') id: number) {
try {
return successResponse(await this.orderService.refundOrder(id));
} catch (error) {
return errorResponse(error?.message || '创建失败');
}
}
@ApiOkResponse()
@Post('/order/completed/:id')
async completedOrder(@Param('id') id: number) {
try {
return successResponse(await this.orderService.completedOrder(id));
} catch (error) {
return errorResponse(error?.message || '创建失败');
}
}
@ApiOkResponse()
@Put('/order/status/:id')
async changeStatus(
@Param('id') id: number,
@Body('status') status: ErpOrderStatus
) {
try {
return successResponse(await this.orderService.changeStatus(id, status));
} catch (error) {
return errorResponse(error?.message || '创建失败');
}
}
@ApiOkResponse()
@Post('/order/create')
async createOrder(@Body() data: Record<string, any>) {
try {
return successResponse(await this.orderService.createOrder(data));
} catch (error) {
return errorResponse(error?.message || '创建失败');
}
}
@ApiOkResponse()
@Post('/order/pengding/items')
async pengdingItems(@Body() data: Record<string, any>) {
try {
return successResponse(await this.orderService.pengdingItems(data));
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
}

View File

@ -0,0 +1,405 @@
import {
Inject,
Post,
Put,
Get,
Body,
Param,
Del,
Query,
Controller,
} from '@midwayjs/core';
import { ProductService } from '../service/product.service';
import { errorResponse, successResponse } from '../utils/response.util';
import {
BatchSetSkuDTO,
CreateCategoryDTO,
CreateFlavorsDTO,
CreateProductDTO,
CreateStrengthDTO,
QueryCategoryDTO,
QueryFlavorsDTO,
QueryProductDTO,
QueryStrengthDTO,
UpdateCategoryDTO,
UpdateFlavorsDTO,
UpdateProductDTO,
UpdateStrengthDTO,
} from '../dto/product.dto';
import { ApiOkResponse } from '@midwayjs/swagger';
import {
BooleanRes,
ProductCatListRes,
ProductCatRes,
ProductListRes,
ProductRes,
ProductsRes,
} from '../dto/reponse.dto';
@Controller('/product')
export class ProductController {
@Inject()
productService: ProductService;
ProductRes;
@ApiOkResponse({
description: '通过name搜索产品',
type: ProductsRes,
})
@Get('/search')
async searchProducts(@Query('name') name: string) {
try {
// 调用服务获取产品数据
const products = await this.productService.findProductsByName(name);
return successResponse(products);
} catch (error) {
return errorResponse(error.message || '获取数据失败');
}
}
@ApiOkResponse({
type: ProductRes,
})
@Get('/sku/:sku')
async productBySku(@Param('sku') sku: string) {
try {
// 调用服务获取产品数据
const product = await this.productService.findProductBySku(sku);
return successResponse(product);
} catch (error) {
return errorResponse(error.message || '获取数据失败');
}
}
@ApiOkResponse({
description: '成功返回产品列表',
type: ProductListRes,
})
@Get('/list')
async getProductList(
@Query() query: QueryProductDTO
): Promise<ProductListRes> {
const { current = 1, pageSize = 10, name, categoryId } = query;
try {
const data = await this.productService.getProductList(
{ current, pageSize },
name,
categoryId
);
return successResponse(data);
} catch (error) {
console.log(error);
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({
type: ProductRes,
})
@Post('/')
async createProduct(@Body() productData: CreateProductDTO) {
try {
const data = this.productService.createProduct(productData);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({
type: ProductRes,
})
@Put('/:id')
async updateProduct(
@Param('id') id: number,
@Body() productData: UpdateProductDTO
) {
try {
const data = this.productService.updateProduct(id, productData);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({
type: ProductRes,
})
@Put('updateNameCn/:id/:nameCn')
async updateProductNameCn(
@Param('id') id: number,
@Param('nameCn') nameCn: string
) {
try {
const data = this.productService.updateProductNameCn(id, nameCn);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({
type: BooleanRes,
})
@Del('/:id')
async deleteProduct(@Param('id') id: number) {
try {
const data = await this.productService.deleteProduct(id);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({
type: ProductCatListRes,
})
@Get('/categories')
async getCategories(@Query() query: QueryCategoryDTO) {
const { current = 1, pageSize = 10, name } = query;
try {
let data = await this.productService.getCategoryList(
{ current, pageSize },
name
);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Get('/categorieAll')
async getCategorieAll() {
try {
let data = await this.productService.getCategoryAll();
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({
type: ProductCatRes,
})
@Post('/category')
async createCategory(@Body() categoryData: CreateCategoryDTO) {
try {
const hasCategory = await this.productService.hasCategory(
categoryData.name
);
if (hasCategory) {
return errorResponse('分类已存在');
}
let data = await this.productService.createCategory(categoryData);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({
type: ProductCatRes,
})
@Put('/category/:id')
async updateCategory(
@Param('id') id: number,
@Body() categoryData: UpdateCategoryDTO
) {
try {
const hasCategory = await this.productService.hasCategory(
categoryData.name
);
if (hasCategory) {
return errorResponse('分类已存在');
}
const data = this.productService.updateCategory(id, categoryData);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({
type: BooleanRes,
})
@Del('/category/:id')
async deleteCategory(@Param('id') id: number) {
try {
const hasProducts = await this.productService.hasProductsInCategory(id);
if (hasProducts) throw new Error('该分类下有商品,无法删除');
const data = await this.productService.deleteCategory(id);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@Post('/batchSetSku')
@ApiOkResponse({
description: '批量设置 sku 的响应结果',
type: BooleanRes,
})
async batchSetSku(@Body() body: BatchSetSkuDTO) {
try {
const result = await this.productService.batchSetSku(body.skus);
return successResponse(result, '批量设置 sku 成功');
} catch (error) {
return errorResponse(error.message, 400);
}
}
@ApiOkResponse()
@Get('/flavorsAll')
async getFlavorsAll() {
try {
let data = await this.productService.getFlavorsAll();
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Get('/flavors')
async getFlavors(@Query() query: QueryFlavorsDTO) {
const { current = 1, pageSize = 10, name } = query;
try {
let data = await this.productService.getFlavorsList(
{ current, pageSize },
name
);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Post('/flavors')
async createFlavors(@Body() flavorsData: CreateFlavorsDTO) {
try {
const hasFlavors = await this.productService.hasFlavors(flavorsData.name);
if (hasFlavors) {
return errorResponse('分类已存在');
}
let data = await this.productService.createFlavors(flavorsData);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Put('/flavors/:id')
async updateFlavors(
@Param('id') id: number,
@Body() flavorsData: UpdateFlavorsDTO
) {
try {
const hasFlavors = await this.productService.hasFlavors(flavorsData.name);
if (hasFlavors) {
return errorResponse('分类已存在');
}
const data = this.productService.updateFlavors(id, flavorsData);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({
type: BooleanRes,
})
@Del('/flavors/:id')
async deleteFlavors(@Param('id') id: number) {
try {
const hasProducts = await this.productService.hasProductsInFlavors(id);
if (hasProducts) throw new Error('该分类下有商品,无法删除');
const data = await this.productService.deleteFlavors(id);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Get('/strengthAll')
async getStrengthAll() {
try {
let data = await this.productService.getStrengthAll();
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Get('/strength')
async getStrength(@Query() query: QueryStrengthDTO) {
const { current = 1, pageSize = 10, name } = query;
try {
let data = await this.productService.getStrengthList(
{ current, pageSize },
name
);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Post('/strength')
async createStrength(@Body() strengthData: CreateStrengthDTO) {
try {
const hasStrength = await this.productService.hasStrength(
strengthData.name
);
if (hasStrength) {
return errorResponse('分类已存在');
}
let data = await this.productService.createStrength(strengthData);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Put('/strength/:id')
async updateStrength(
@Param('id') id: number,
@Body() strengthData: UpdateStrengthDTO
) {
try {
const hasStrength = await this.productService.hasStrength(
strengthData.name
);
if (hasStrength) {
return errorResponse('分类已存在');
}
const data = this.productService.updateStrength(id, strengthData);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({
type: BooleanRes,
})
@Del('/strength/:id')
async deleteStrength(@Param('id') id: number) {
try {
const hasProducts = await this.productService.hasProductsInStrength(id);
if (hasProducts) throw new Error('该分类下有商品,无法删除');
const data = await this.productService.deleteStrength(id);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
}

View File

@ -0,0 +1,75 @@
import { Body, Controller, Get, Inject, Param, Put, Post, Query } from '@midwayjs/core';
import { ApiOkResponse } from '@midwayjs/swagger';
import { WpSitesResponse } from '../dto/reponse.dto';
import { errorResponse, successResponse } from '../utils/response.util';
import { SiteService } from '../service/site.service';
import { CreateSiteDTO, DisableSiteDTO, QuerySiteDTO, UpdateSiteDTO } from '../dto/site.dto';
@Controller('/site')
export class SiteController {
@Inject()
siteService: SiteService;
@ApiOkResponse({ description: '关联网站', type: WpSitesResponse })
@Get('/all')
async all() {
try {
const { items } = await this.siteService.list({ current: 1, pageSize: 1000, isDisabled: false });
return successResponse(items.map((v: any) => ({ id: v.id, siteName: v.siteName })));
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@Post('/create')
async create(@Body() body: CreateSiteDTO) {
try {
await this.siteService.create(body);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || '创建失败');
}
}
@Put('/update/:id')
async update(@Param('id') id: string, @Body() body: UpdateSiteDTO) {
try {
await this.siteService.update(Number(id), body);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || '更新失败');
}
}
@Get('/get/:id')
async get(@Param('id') id: string) {
try {
const data = await this.siteService.get(Number(id), false);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@Get('/list')
async list(@Query() query: QuerySiteDTO) {
try {
const data = await this.siteService.list(query, false);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
// 批量查询改为使用 /site/list?ids=1,2,3
@Put('/disable/:id')
async disable(@Param('id') id: string, @Body() body: DisableSiteDTO) {
try {
await this.siteService.disable(Number(id), body.disabled);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || '更新失败');
}
}
}

View File

@ -0,0 +1,99 @@
import { Body, Controller, Get, Inject, Post, Query } from '@midwayjs/core';
import { StatisticsService } from '../service/statistics.service';
import { OrderStatisticsParams } from '../dto/statistics.dto';
import { errorResponse, successResponse } from '../utils/response.util';
import { ApiOkResponse } from '@midwayjs/swagger';
@Controller('/statistics')
export class StatisticsController {
@Inject()
statisticsService: StatisticsService;
@ApiOkResponse()
@Post('/order')
async getOrderStatistics(@Body() params: OrderStatisticsParams) {
try {
return successResponse(
await this.statisticsService.getOrderStatistics(params)
);
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse()
@Post('/orderByDate')
async getOrderByDate(@Body('date') date: string) {
try {
return successResponse(await this.statisticsService.getOrderByDate(date));
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse()
@Post('/orderByEmail')
async getOrderByEmail(@Body('email') email: string) {
try {
return successResponse(
await this.statisticsService.getOrderByEmail(email)
);
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse()
@Post('/getCustomerOrders')
async getCustomerOrders(@Body('month') month) {
try {
return successResponse(
await this.statisticsService.getCustomerOrders(month)
);
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse()
@Post('/stockForecast')
async stockForecast(@Body() params) {
try {
return successResponse(
await this.statisticsService.stockForecast(params)
);
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse()
@Post('/restocking')
async restocking(@Body() params) {
try {
return successResponse(await this.statisticsService.restocking(params));
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse()
@Get('/orderSource')
async getOrderSorce(@Query() params) {
try {
return successResponse(await this.statisticsService.getOrderSorce(params));
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse()
@Get('/inactiveUsersByMonth')
async getInativeUsersByMonth(@Query('month') month: string) {
try {
return successResponse(await this.statisticsService.getInativeUsersByMonth(month));
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
}

View File

@ -0,0 +1,273 @@
import {
Controller,
Post,
Get,
Body,
Inject,
Del,
Param,
Query,
Put,
} from '@midwayjs/core';
import { StockService } from '../service/stock.service';
import { errorResponse, successResponse } from '../utils/response.util';
import {
CreatePurchaseOrderDTO,
CreateStockPointDTO,
QueryPointDTO,
QueryPurchaseOrderDTO,
QueryStockDTO,
QueryStockRecordDTO,
UpdatePurchaseOrderDTO,
UpdateStockDTO,
UpdateStockPointDTO,
} from '../dto/stock.dto';
import { ApiOkResponse } from '@midwayjs/swagger';
import {
BooleanRes,
PurchaseOrderListRes,
StockListRes,
StockPointAllRespone,
StockPointListRes,
StockRecordListRes,
} from '../dto/reponse.dto';
import { User } from '../decorator/user.decorator';
@Controller('/stock')
export class StockController {
@Inject()
private readonly stockService: StockService;
@ApiOkResponse({ type: BooleanRes, description: '创建库存点' })
@Post('/stock-point')
async createStockPoint(@Body() body: CreateStockPointDTO) {
try {
const data = await this.stockService.createStockPoint(body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '获取库存列表失败');
}
}
@ApiOkResponse({ type: BooleanRes, description: '创建库存点' })
@Put('/stock-point/:id')
async updateStockPoint(
@Param('id') id: number,
@Body() body: UpdateStockPointDTO
) {
try {
const data = await this.stockService.updateStockPoint(id, body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '获取库存列表失败');
}
}
@ApiOkResponse({ type: StockPointListRes, description: '获取库存点列表' })
@Get('/stock-point')
async getStockPoints(@Query() query: QueryPointDTO) {
try {
const data = await this.stockService.getStockPoints(query);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '获取库存列表失败');
}
}
@ApiOkResponse({ type: StockPointAllRespone, description: '获取所有库存' })
@Get('/stock-point/all')
async getAllStockPoints() {
try {
const data = await this.stockService.getAllStockPoints();
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse({ type: BooleanRes, description: '删除库存点' })
@Del('/stock-point/:id')
async delStockPoints(@Param('id') id: number) {
try {
await this.stockService.delStockPoints(id);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || '删除库存点失败');
}
}
@ApiOkResponse({ type: BooleanRes })
@Post('/purchase-order')
async createPurchaseOrder(@Body() body: CreatePurchaseOrderDTO) {
try {
const data = await this.stockService.createPurchaseOrder(body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '创建失败');
}
}
@ApiOkResponse({ type: BooleanRes })
@Put('/purchase-order/:id')
async updatePurchaseOrder(
@Param('id') id: number,
@Body() body: UpdatePurchaseOrderDTO
) {
try {
const data = await this.stockService.updatePurchaseOrder(id, body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '更新失败');
}
}
@ApiOkResponse({ type: PurchaseOrderListRes, description: '获取采购列表' })
@Get('/purchase-order')
async getPurchaseOrders(@Query() query: QueryPurchaseOrderDTO) {
try {
const data = await this.stockService.getPurchaseOrders(query);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '获取库存记录列表失败');
}
}
@ApiOkResponse({ type: BooleanRes })
@Del('/purchase-order/:id')
async delPurchaseOrder(@Param('id') id: number) {
try {
const data = await this.stockService.delPurchaseOrder(id);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '删除失败');
}
}
@ApiOkResponse({ type: BooleanRes })
@Post('/purchase-order/:id')
async receivePurchaseOrder(@Param('id') id: number, @User() user) {
try {
const data = await this.stockService.receivePurchaseOrder(id, user.id);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '更新失败');
}
}
@ApiOkResponse({ type: BooleanRes })
@Get('/purchase-order/:orderNumber')
async getPurchaseOrder(@Param('orderNumber') orderNumber: string) {
try {
const data = await this.stockService.getPurchaseOrder(orderNumber);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '更新失败');
}
}
@ApiOkResponse({ type: StockListRes, description: '获取库存列表' })
@Get('/')
async getStocks(@Query() query: QueryStockDTO) {
try {
const data = await this.stockService.getStocks(query);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '获取库存列表失败');
}
}
@ApiOkResponse({
type: BooleanRes,
description: '更新库存(入库、出库、调整)',
})
@Post('/update')
async updateStock(@Body() body: UpdateStockDTO) {
try {
await this.stockService.updateStock(body);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || '更新库存失败');
}
}
@ApiOkResponse({ type: StockRecordListRes, description: '获取库存记录列表' })
@Get('/records')
async getStockRecords(@Query() query: QueryStockRecordDTO) {
try {
const data = await this.stockService.getStockRecords(query);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '获取库存记录列表失败');
}
}
@ApiOkResponse({ type: BooleanRes })
@Post('/transfer')
async createTransfer(@Body() body: Record<string, any>, @User() user) {
try {
const data = await this.stockService.createTransfer(body, user.id);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '创建失败');
}
}
@ApiOkResponse()
@Get('/transfer')
async getTransfers(@Query() query: Record<string, any>) {
try {
const data = await this.stockService.getTransfers(query);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '获取调拨列表失败');
}
}
@ApiOkResponse({ type: BooleanRes })
@Post('/cancelTransfer/:id')
async cancelTransfer(@Param('id') id: number, @User() user) {
try {
const data = await this.stockService.cancelTransfer(id, user.id);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '删除失败');
}
}
@ApiOkResponse({ type: BooleanRes })
@Post('/receiveTransfer/:id')
async receiveTransfer(@Param('id') id: number, @User() user) {
try {
const data = await this.stockService.receiveTransfer(id, user.id);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '更新失败');
}
}
@ApiOkResponse({ type: BooleanRes })
@Post('/lostTransfer/:id')
async lostTransfer(@Param('id') id: number) {
try {
const data = await this.stockService.lostTransfer(id);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '更新失败');
}
}
@ApiOkResponse({ type: BooleanRes })
@Put('/receiveTransfer/:id')
async updateTransfer(
@Param('id') id: number,
@Body() body: Record<string, any>,
@User() user
) {
try {
const data = await this.stockService.updateTransfer(id, body, user.id);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '更新失败');
}
}
}

View File

@ -0,0 +1,36 @@
import { Controller, Inject, Param, Post, Get, Query } from '@midwayjs/core';
import { ApiOkResponse } from '@midwayjs/swagger';
import { SubscriptionService } from '../service/subscription.service';
import { errorResponse, successResponse } from '../utils/response.util';
import { BooleanRes, SubscriptionListRes } from '../dto/reponse.dto';
import { QuerySubscriptionDTO } from '../dto/subscription.dto';
@Controller('/subscription')
export class SubscriptionController {
@Inject()
subscriptionService: SubscriptionService;
// 同步订阅:根据站点 ID 拉取并更新本地订阅数据
@ApiOkResponse({ type: BooleanRes })
@Post('/sync/:siteId')
async sync(@Param('siteId') siteId: string) {
try {
await this.subscriptionService.syncSubscriptions(siteId);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || '同步失败');
}
}
// 订阅列表:分页 + 筛选
@ApiOkResponse({ type: SubscriptionListRes })
@Get('/list')
async list(@Query() query: QuerySubscriptionDTO) {
try {
const data = await this.subscriptionService.getSubscriptionList(query);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
}

View File

@ -0,0 +1,74 @@
// src/controller/user.controller.ts
import { Controller, Post, Get, Body, Query } from '@midwayjs/core';
import { Inject } from '@midwayjs/decorator';
import { UserService } from '../service/user.service';
import { errorResponse, successResponse } from '../utils/response.util';
import { ApiOkResponse } from '@midwayjs/swagger';
import { BooleanRes, LoginRes } from '../dto/reponse.dto';
import { User } from '../decorator/user.decorator';
@Controller('/user')
export class UserController {
@Inject()
userService: UserService;
@Inject()
ctx;
@ApiOkResponse({
type: LoginRes,
})
@Post('/login')
async login(@Body() body) {
this.ctx.logger.info('ip:', this.ctx.ip, '; path:', this.ctx.path, '; user:', body?.username);
try {
const result = await this.userService.login(body);
return successResponse(result, '登录成功');
} catch (error) {
return errorResponse(error?.message || '登录失败', error?.code);
}
}
@ApiOkResponse({
type: BooleanRes,
})
@Post('/logout')
async logout() {
// 可选:在这里处理服务端缓存的 token 或 session
return successResponse(true);
}
@Post('/add')
async addUser(@Body() body: { username: string; password: string }) {
const { username, password } = body;
try {
await this.userService.addUser(username, password);
return successResponse(true);
} catch (error) {
console.log(error);
return errorResponse('添加用户失败');
}
}
@Get('/list')
async listUsers(@Query() query: { current: number; pageSize: number }) {
const { current = 1, pageSize = 10 } = query;
return successResponse(await this.userService.listUsers(current, pageSize));
}
@Post('/toggleActive')
async toggleActive(@Body() body: { userId: number; isActive: boolean }) {
return this.userService.toggleUserActive(body.userId, body.isActive);
}
@ApiOkResponse()
@Get()
async getUser(@User() user) {
try {
return successResponse(await this.userService.getUser(user.id));
} catch (error) {
return errorResponse('获取失败');
}
}
}

View File

@ -0,0 +1,140 @@
import { HttpStatus, Inject } from '@midwayjs/core';
import {
Controller,
Post,
Body,
Headers,
Get,
Query,
} from '@midwayjs/decorator';
import { Context } from '@midwayjs/koa';
import * as crypto from 'crypto';
import { WpProductService } from '../service/wp_product.service';
import { WPService } from '../service/wp.service';
import { SiteService } from '../service/site.service';
import { OrderService } from '../service/order.service';
@Controller('/webhook')
export class WebhookController {
private secret = 'YOONE24kd$kjcdjflddd';
@Inject()
private readonly wpProductService: WpProductService;
@Inject()
private readonly wpApiService: WPService;
@Inject()
private readonly orderService: OrderService;
@Inject()
ctx: Context;
@Inject()
private readonly siteService: SiteService;
// 移除配置中的站点数组,来源统一改为数据库
@Get('/')
async test() {
return 'webhook';
}
@Post('/woocommerce')
async handleWooWebhook(
@Body() body: any,
@Query('siteId') siteId: string,
@Headers() header: any
) {
const signature = header['x-wc-webhook-signature'];
const topic = header['x-wc-webhook-topic'];
const source = header['x-wc-webhook-source'];
// 从数据库获取站点配置
const site = await this.siteService.get(Number(siteId), true);
if (!site || !source.includes(site.apiUrl)) {
console.log('domain not match');
return {
code: HttpStatus.BAD_REQUEST,
success: false,
message: 'domain not match',
};
}
if (!signature) {
return {
code: HttpStatus.BAD_REQUEST,
success: false,
message: 'Signature missing',
};
}
const rawBody = this.ctx.request.rawBody;
const hash = crypto
.createHmac('sha256', this.secret)
.update(rawBody)
.digest('base64');
try {
if (hash === signature) {
switch (topic) {
case 'product.created':
case 'product.updated':
// 变体更新
if (body.type === 'variation') {
const variation = await this.wpApiService.getVariation(
site,
body.parent_id,
body.id
);
this.wpProductService.syncVariation(
siteId,
body.parent_id,
variation
);
break;
}
const variations =
body.type === 'variable'
? await this.wpApiService.getVariations(site, body.id)
: [];
await this.wpProductService.syncProductAndVariations(
String(site.id),
body,
variations
);
break;
case 'product.deleted':
await this.wpProductService.delWpProduct(String(site.id), body.id);
break;
case 'order.created':
case 'order.updated':
await this.orderService.syncSingleOrder(siteId, body);
break;
case 'order.deleted':
break;
case 'customer.created':
break;
case 'customer.updated':
break;
case 'customer.deleted':
break;
default:
console.log('Unhandled event:', body.event);
}
return {
code: 200,
success: true,
message: 'Webhook processed successfully',
};
} else {
return {
code: 403,
success: false,
message: 'Webhook verification failed',
};
}
} catch (error) {
console.log(error);
}
}
}

View File

@ -0,0 +1,200 @@
import {
Controller,
Param,
Post,
Inject,
Get,
Query,
Put,
Body,
} from '@midwayjs/core';
import { WpProductService } from '../service/wp_product.service';
import { errorResponse, successResponse } from '../utils/response.util';
import { ApiOkResponse } from '@midwayjs/swagger';
import { BooleanRes, WpProductListRes } from '../dto/reponse.dto';
import {
QueryWpProductDTO,
SetConstitutionDTO,
UpdateVariationDTO,
UpdateWpProductDTO,
} from '../dto/wp_product.dto';
import { WPService } from '../service/wp.service';
import { SiteService } from '../service/site.service';
import {
ProductsRes,
} from '../dto/reponse.dto';
@Controller('/wp_product')
export class WpProductController {
// 移除控制器内的配置站点引用,统一由服务层处理站点数据
@Inject()
private readonly wpProductService: WpProductService;
@Inject()
private readonly wpApiService: WPService;
@Inject()
private readonly siteService: SiteService;
@ApiOkResponse({
type: BooleanRes,
})
@Post('/sync/:siteId')
async syncProducts(@Param('siteId') siteId: string) {
try {
await this.wpProductService.syncSite(siteId);
return successResponse(true);
} catch (error) {
console.log(error);
return errorResponse('同步失败');
}
}
@ApiOkResponse({
type: WpProductListRes,
})
@Get('/list')
async getWpProducts(@Query() query: QueryWpProductDTO) {
try {
const data = await this.wpProductService.getProductList(query);
return successResponse(data);
} catch (error) {
return errorResponse(error.message);
}
}
@ApiOkResponse({
type: BooleanRes,
})
@Put('/:id/constitution')
async setConstitution(
@Param('id') id: number,
@Body()
body: SetConstitutionDTO
) {
const { isProduct, constitution } = body;
try {
await this.wpProductService.setConstitution(id, isProduct, constitution);
return successResponse(true);
} catch (error) {
return errorResponse(error.message);
}
}
@ApiOkResponse({
type: BooleanRes
})
@Post('/updateState/:id')
async updateWPProductState(
@Param('id') id: number,
@Body() body: any, // todo
) {
try {
const res = await this.wpProductService.updateProductStatus(id, body?.status, body?.stock_status);
return successResponse(res);
} catch (error) {
return errorResponse(error.message);
}
}
/**
*
* @param productId ID
* @param body
*/
@ApiOkResponse({
type: BooleanRes,
})
@Put('/siteId/:siteId/products/:productId')
async updateProduct(
@Param('siteId') siteId: string,
@Param('productId') productId: string,
@Body() body: UpdateWpProductDTO
) {
try {
const isDuplicate = await this.wpProductService.isSkuDuplicate(
body.sku,
siteId,
productId
);
if (isDuplicate) {
return errorResponse('SKU已存在');
}
const site = await this.siteService.get(Number(siteId), true);
const result = await this.wpApiService.updateProduct(
site,
productId,
body
);
if (result) {
this.wpProductService.updateWpProduct(siteId, productId, body);
return successResponse(result, '产品更新成功');
}
return errorResponse('产品更新失败');
} catch (error) {
console.error('更新产品失败:', error);
return errorResponse(error.message || '产品更新失败');
}
}
/**
*
* @param productId ID
* @param variationId ID
* @param body
*/
@Put('/siteId/:siteId/products/:productId/variations/:variationId')
async updateVariation(
@Param('siteId') siteId: string,
@Param('productId') productId: string,
@Param('variationId') variationId: string,
@Body() body: UpdateVariationDTO
) {
try {
const isDuplicate = await this.wpProductService.isSkuDuplicate(
body.sku,
siteId,
productId,
variationId
);
if (isDuplicate) {
return errorResponse('SKU已存在');
}
const site = await this.siteService.get(Number(siteId), true);
const result = await this.wpApiService.updateVariation(
site,
productId,
variationId,
body
);
if (result) {
this.wpProductService.updateWpProductVaritation(
siteId,
productId,
variationId,
body
);
return successResponse(result, '产品变体更新成功');
}
return errorResponse('变体更新失败');
} catch (error) {
console.error('更新变体失败:', error);
return errorResponse(error.message || '产品变体更新失败');
}
}
@ApiOkResponse({
description: '通过name搜索产品/订单',
type: ProductsRes,
})
@Get('/search')
async searchProducts(@Query('name') name: string) {
try {
// 调用服务获取产品数据
const products = await this.wpProductService.findProductsByName(name);
return successResponse(products);
} catch (error) {
return errorResponse(error.message || '获取数据失败');
}
}
}

View File

@ -0,0 +1,8 @@
import { createCustomParamDecorator } from '@midwayjs/core';
export const USER_KEY = 'USER_KEY';
// 定义装饰器
export function User(): ParameterDecorator {
return createCustomParamDecorator(USER_KEY, {});
}

38
src/dto/customer.dto.ts Normal file
View File

@ -0,0 +1,38 @@
import { ApiProperty } from '@midwayjs/swagger';
export class QueryCustomerListDTO {
@ApiProperty()
current: string;
@ApiProperty()
pageSize: string;
@ApiProperty()
email: string;
@ApiProperty()
tags: string;
@ApiProperty()
sorterKey: string;
@ApiProperty()
sorterValue: string;
@ApiProperty()
state: string;
@ApiProperty()
first_purchase_date: string;
@ApiProperty()
customerId: number;
}
export class CustomerTagDTO {
@ApiProperty()
email: string;
@ApiProperty()
tag: string;
}

306
src/dto/freightcom.dto.ts Normal file
View File

@ -0,0 +1,306 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate';
// 定义包装类型的联合类型
export type PackagingType =
// | PackagingPallet
PackagingPackage;
// | PackagingCourierPak
// | PackagingEnvelope;
// 定义包装类型的枚举,用于 API 文档描述
export enum PackagingTypeEnum {
Pallet = 'pallet',
Package = 'package',
CourierPak = 'courier-pak',
Envelope = 'envelope',
}
export class Address {
@ApiProperty()
@Rule(RuleType.string())
address_line_1: string;
@ApiProperty()
@Rule(RuleType.string())
city: string;
@ApiProperty()
@Rule(RuleType.string())
region: string;
@ApiProperty()
@Rule(RuleType.string())
country: string;
@ApiProperty()
@Rule(RuleType.string())
postal_code: string;
}
export class PhoneNumber {
@ApiProperty()
@Rule(RuleType.string())
number: string;
@ApiProperty()
@Rule(RuleType.string())
extension: string;
}
export class Location {
@ApiProperty()
@Rule(RuleType.string())
name: string;
@ApiProperty({ type: Address })
@Rule(RuleType.object<Address>())
address: Address;
@ApiProperty({ type: PhoneNumber })
@Rule(RuleType.object<PhoneNumber>())
phone_number: PhoneNumber;
@ApiProperty()
@Rule(RuleType.array<string>())
email_addresses: string[];
contact_name?: string;
}
export class Time {
@ApiProperty()
@Rule(RuleType.string())
hour: string;
@ApiProperty()
@Rule(RuleType.string())
minute: string;
}
export enum SignatureRequirementEnum {
NOTREQUIRED = 'not-required',
REQUIRED = 'required',
ADULTREQUIRED = 'adult-required',
}
export class Destination extends Location {
@ApiProperty({ type: Time })
@Rule(RuleType.object<Time>())
ready_at: Time;
@ApiProperty({ type: Time })
@Rule(RuleType.object<Time>())
ready_until: Time;
@ApiProperty({ type: SignatureRequirementEnum })
@Rule(RuleType.string().valid(...Object.values(SignatureRequirementEnum)))
SignatureRequirementEnum: SignatureRequirementEnum;
}
export class Date {
@ApiProperty()
@Rule(RuleType.string())
year: string;
@ApiProperty()
@Rule(RuleType.string())
month: string;
@ApiProperty()
@Rule(RuleType.string())
day: string;
}
export enum UnitEnum {
KG = 'kg',
LB = 'lb',
G = 'g',
OZ = 'oz',
}
export class Cubid {
@ApiProperty()
@Rule(RuleType.number())
w: number;
@ApiProperty()
@Rule(RuleType.number())
h: number;
@ApiProperty()
@Rule(RuleType.number())
l: number;
@ApiProperty()
@Rule(RuleType.string())
unit: string;
}
export class Weight {
@ApiProperty({ enum: UnitEnum })
@Rule(RuleType.string().valid(...Object.values(UnitEnum)))
unit: UnitEnum;
@ApiProperty()
@Rule(RuleType.number())
value: number;
}
export class Measurements {
@ApiProperty({ type: Cubid })
cuboid: Cubid;
@ApiProperty({ type: Cubid })
weight: Weight;
}
export class Pallets {
@ApiProperty({ type: Measurements })
@Rule(RuleType.object<Measurements>())
measurements: Measurements;
@ApiProperty()
@Rule(RuleType.string())
description: string;
@ApiProperty()
@Rule(RuleType.string())
freight_class: string;
}
export class PackagingPallet {
@ApiProperty()
@Rule(RuleType.string())
pallet_type: string;
@ApiProperty({ type: Pallets })
@Rule(RuleType.object<Pallets>())
pallets: Pallets;
}
export class Package {
@ApiProperty({ type: Measurements })
@Rule(RuleType.object<Measurements>())
measurements: Measurements;
@ApiProperty()
@Rule(RuleType.string())
description: string;
}
export class PackagingPackage {
@ApiProperty({ type: Package, isArray: true })
@Rule(RuleType.array<Package>())
packages: Package[];
}
export class PackagingCourierPak {
@ApiProperty({ type: Package, isArray: true })
@Rule(RuleType.array<Package>())
courier_paks: Package[];
}
export class PackagingEnvelope {
// 添加必要的属性和装饰器
@ApiProperty()
@Rule(RuleType.boolean())
includes_return_label?: boolean;
}
// export enum InsuranceTypeEnum {
// INTERNAL = 'internal',
// CARRIER = 'carrier',
// }
// export class Insurance {
// @ApiProperty({ enum: InsuranceTypeEnum })
// @Rule(RuleType.string().valid(...Object.values(InsuranceTypeEnum)))
// type: InsuranceTypeEnum;
// }
export class ShippingDetailsDTO {
@ApiProperty()
shipmentFee: number;
@ApiProperty({ type: Location })
@Rule(RuleType.object<Location>())
origin: Location;
@ApiProperty({ type: Destination })
@Rule(RuleType.object<Destination>())
destination: Destination;
@ApiProperty({ type: Date })
@Rule(RuleType.object<Date>())
expected_ship_date: Date;
@ApiProperty({ enum: PackagingTypeEnum })
@Rule(RuleType.string().valid(...Object.values(PackagingTypeEnum)))
packaging_type: PackagingTypeEnum;
@ApiProperty({
type: () => [
// PackagingPallet,
PackagingPackage,
// PackagingCourierPak,
// PackagingEnvelope,
],
})
@Rule(RuleType.object<PackagingType>())
packaging_properties: PackagingType;
// @ApiProperty({ type: Insurance })
// @Rule(RuleType.object<Insurance>())
// insurance?: Insurance;
@ApiProperty()
reference_codes: string[];
}
export class Money {
@ApiProperty()
currency: string;
@ApiProperty()
value: string;
}
export class Surcharges {
@ApiProperty()
type: string;
@ApiProperty({ type: Money })
amount: Money;
}
export class RateDTO {
@ApiProperty()
carrier_name: string;
@ApiProperty()
service_name: string;
@ApiProperty()
service_id: string;
@ApiProperty({ type: Date })
valid_until: Date;
@ApiProperty({ type: Money })
total: Money;
@ApiProperty({ type: Money })
base: Money;
@ApiProperty({ type: Surcharges, isArray: true })
surcharges: Surcharges[];
@ApiProperty({ type: Money, isArray: true })
taxes: Money[];
@ApiProperty()
transit_time_days: number;
@ApiProperty()
transit_time_not_available: boolean;
}

109
src/dto/logistics.dto.ts Normal file
View File

@ -0,0 +1,109 @@
import { ApiProperty } from '@midwayjs/swagger';
import { ShippingDetailsDTO } from './freightcom.dto';
import { Rule, RuleType } from '@midwayjs/validate';
import { OrderSale } from '../entity/order_sale.entity';
export class ShipmentBookDTO {
@ApiProperty({ type: OrderSale, isArray: true })
@Rule(RuleType.array<OrderSale>())
sales: OrderSale[];
@ApiProperty({ type: ShippingDetailsDTO })
@Rule(RuleType.object<ShippingDetailsDTO>())
details: ShippingDetailsDTO;
@ApiProperty()
@Rule(RuleType.number())
stockPointId: number;
@ApiProperty({ type: 'number', isArray: true })
@Rule(RuleType.array<number>().default([]))
orderIds?: number[];
@ApiProperty()
@Rule(RuleType.string())
shipmentPlatform: string;
@ApiProperty()
@Rule(RuleType.any())
courierCompany: string;
}
export class ShipmentFeeBookDTO {
@ApiProperty()
shipmentPlatform: string;
@ApiProperty()
courierCompany: string;
@ApiProperty()
stockPointId: number;
@ApiProperty()
sender: string;
@ApiProperty()
startPhone: string|any;
@ApiProperty()
startPostalCode: string;
@ApiProperty()
pickupAddress: string;
// pickupWarehouse: number; // 此处用 stockPointId 到后端解析
@ApiProperty()
shipperCountryCode: string;
@ApiProperty()
receiver: string;
@ApiProperty()
city: string;
@ApiProperty()
province: string;
@ApiProperty()
country: string;
@ApiProperty()
postalCode: string;
@ApiProperty()
deliveryAddress: string;
@ApiProperty()
receiverPhone: string;
@ApiProperty()
receiverEmail: string;
@ApiProperty()
length: number;
@ApiProperty()
width: number;
@ApiProperty()
height: number;
@ApiProperty()
dimensionUom: string;
@ApiProperty()
weight: number;
@ApiProperty()
weightUom: string;
@ApiProperty()
address_id: number;
}
export class PaymentMethodDTO {
@ApiProperty()
id: string;
@ApiProperty()
type: string;
@ApiProperty()
label: string;
}
export class QueryServiceDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number;
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number;
@ApiProperty()
@Rule(RuleType.string())
carrier_name: string;
@ApiProperty()
@Rule(RuleType.bool())
isActive: boolean;
}

181
src/dto/order.dto.ts Normal file
View File

@ -0,0 +1,181 @@
import { ApiProperty } from '@midwayjs/swagger';
import { ErpOrderStatus } from '../enums/base.enum';
import { Rule, RuleType } from '@midwayjs/validate';
// import { Shipment } from '../entity/shipment.entity';
// import { ShipmentItem } from '../entity/shipment_item.entity';
export class OrderAddress {
@ApiProperty()
first_name: string;
@ApiProperty()
last_name: string;
@ApiProperty()
company: string;
@ApiProperty()
address_1: string;
@ApiProperty()
address_2: string;
@ApiProperty()
city: string;
@ApiProperty()
state: string;
@ApiProperty()
postcode: string;
@ApiProperty()
country: string;
@ApiProperty()
email: string;
@ApiProperty()
phone: string;
}
export class OrderStatusCountDTO {
@ApiProperty()
status: ErpOrderStatus;
@ApiProperty()
count: number;
}
export class QueryOrderDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number;
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number;
@ApiProperty()
@Rule(RuleType.string())
externalOrderId: string;
@ApiProperty()
@Rule(RuleType.string())
siteId: string;
@ApiProperty()
@Rule(RuleType.string().allow(''))
customer_email: string;
@ApiProperty()
@Rule(RuleType.string().allow(''))
billing_phone: string;
@ApiProperty()
@Rule(RuleType.string().allow(null))
keyword: string;
@ApiProperty()
@Rule(RuleType.date())
startDate: Date;
@ApiProperty()
@Rule(RuleType.date())
endDate: Date;
@ApiProperty({ type: 'enum', enum: ErpOrderStatus })
@Rule(RuleType.string().valid(...Object.values(ErpOrderStatus)))
status: ErpOrderStatus;
@ApiProperty()
@Rule(RuleType.string())
payment_method: string;
@ApiProperty({ description: '仅订阅订单(父订阅订单或包含订阅商品)' })
@Rule(RuleType.bool().default(false))
isSubscriptionOnly?: boolean;
}
export class QueryOrderSalesDTO {
@ApiProperty()
@Rule(RuleType.bool().default(false))
isSource: boolean;
@ApiProperty()
@Rule(RuleType.bool().default(false))
exceptPackage: boolean;
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number;
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number;
@ApiProperty()
@Rule(RuleType.string())
siteId: string;
@ApiProperty()
@Rule(RuleType.string())
name: string;
@ApiProperty()
@Rule(RuleType.date())
startDate: Date;
@ApiProperty()
@Rule(RuleType.date())
endDate: Date;
}
// export class Tracking extends Shipment {
// @ApiProperty({ type: ShipmentItem, isArray: true })
// products?: ShipmentItem[];
// }
export class CreateOrderNoteDTO {
@ApiProperty()
@Rule(RuleType.number())
orderId: number;
@ApiProperty()
@Rule(RuleType.string())
content: string;
}
export class QueryOrderItemDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number;
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number;
@ApiProperty()
@Rule(RuleType.string().allow(''))
siteId: string;
@ApiProperty()
@Rule(RuleType.string().allow(''))
name: string; // 商品名称关键字
@ApiProperty()
@Rule(RuleType.string().allow(''))
externalProductId: string;
@ApiProperty()
@Rule(RuleType.string().allow(''))
externalVariationId: string;
@ApiProperty()
@Rule(RuleType.date())
startDate: Date;
@ApiProperty()
@Rule(RuleType.date())
endDate: Date;
}

174
src/dto/product.dto.ts Normal file
View File

@ -0,0 +1,174 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate';
/**
* DTO
*/
export class CreateProductDTO {
@ApiProperty({
example: 'ZYN 6MG WINTERGREEN',
description: '产品名称',
required: true,
})
@Rule(RuleType.string().required().empty({ message: '产品名称不能为空' }))
name: string;
@ApiProperty({ example: '产品描述', description: '产品描述' })
@Rule(RuleType.string())
description: string;
@ApiProperty({ example: '1', description: '分类 ID' })
@Rule(RuleType.number())
categoryId: number;
@ApiProperty()
@Rule(RuleType.number())
strengthId: number;
@ApiProperty()
@Rule(RuleType.number())
flavorsId: number;
@ApiProperty()
@Rule(RuleType.string())
humidity: string;
}
/**
* DTO
*/
export class UpdateProductDTO extends CreateProductDTO {
@ApiProperty({ example: 'ZYN 6MG WINTERGREEN', description: '产品名称' })
@Rule(RuleType.string())
name: string;
}
/**
* DTO
*/
export class QueryProductDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number;
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number;
@ApiProperty({ example: 'ZYN', description: '关键字' })
@Rule(RuleType.string())
name: string;
@ApiProperty({ example: '1', description: '分类 ID' })
@Rule(RuleType.string())
categoryId: number;
}
/**
* DTO
*/
export class CreateCategoryDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称', required: true })
@Rule(RuleType.string().required().empty({ message: '分类名称不能为空' }))
name: string;
@Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
unique_key: string;
}
/**
* DTO
*/
export class UpdateCategoryDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称' })
@Rule(RuleType.string())
name: string;
}
/**
* DTO
*/
export class QueryCategoryDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number; // 页码
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number; // 每页大小
@ApiProperty({ example: 'ZYN', description: '关键字' })
@Rule(RuleType.string())
name: string; // 搜索关键字(支持模糊查询)
}
export class CreateFlavorsDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称', required: true })
@Rule(RuleType.string().required().empty({ message: '分类名称不能为空' }))
name: string;
@Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
unique_key: string;
}
export class UpdateFlavorsDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称' })
@Rule(RuleType.string())
name: string;
}
export class QueryFlavorsDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number; // 页码
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number; // 每页大小
@ApiProperty({ example: 'ZYN', description: '关键字' })
@Rule(RuleType.string())
name: string; // 搜索关键字(支持模糊查询)
}
export class CreateStrengthDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称', required: true })
@Rule(RuleType.string().required().empty({ message: '分类名称不能为空' }))
name: string;
@Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
unique_key: string;
}
export class UpdateStrengthDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称' })
@Rule(RuleType.string())
name: string;
}
export class QueryStrengthDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number; // 页码
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number; // 每页大小
@ApiProperty({ example: 'ZYN', description: '关键字' })
@Rule(RuleType.string())
name: string; // 搜索关键字(支持模糊查询)
}
export class SkuItemDTO {
@ApiProperty({ description: '产品 ID' })
productId: number;
@ApiProperty({ description: 'sku 编码' })
sku: string;
}
export class BatchSetSkuDTO {
@ApiProperty({ description: 'sku 数据列表', type: [SkuItemDTO] })
skus: SkuItemDTO[];
}

125
src/dto/reponse.dto.ts Normal file
View File

@ -0,0 +1,125 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Category } from '../entity/category.entity';
import { Order } from '../entity/order.entity';
import { Product } from '../entity/product.entity';
import { StockPoint } from '../entity/stock_point.entity';
import { PaginatedWrapper } from '../utils/paginated-response.util';
import {
SuccessArrayWrapper,
SuccessWrapper,
} from '../utils/response-wrapper.util';
import { OrderStatusCountDTO } from './order.dto';
import { SiteConfig } from './site.dto';
import { PurchaseOrderDTO, StockDTO, StockRecordDTO } from './stock.dto';
import { LoginResDTO } from './user.dto';
import { WpProductDTO } from './wp_product.dto';
import { OrderSale } from '../entity/order_sale.entity';
import { Service } from '../entity/service.entity';
import { RateDTO } from './freightcom.dto';
import { ShippingAddress } from '../entity/shipping_address.entity';
import { OrderItem } from '../entity/order_item.entity';
import { OrderRefundItem } from '../entity/order_retund_item.entity';
import { OrderNote } from '../entity/order_note.entity';
import { PaymentMethodDTO } from './logistics.dto';
import { Flavors } from '../entity/flavors.entity';
import { Strength } from '../entity/strength.entity';
import { Subscription } from '../entity/subscription.entity';
export class BooleanRes extends SuccessWrapper(Boolean) {}
//网站配置返回数据
export class WpSitesResponse extends SuccessArrayWrapper(SiteConfig) {}
//产品分页数据
export class ProductPaginatedResponse extends PaginatedWrapper(Product) {}
//产品分页返回数据
export class ProductListRes extends SuccessWrapper(ProductPaginatedResponse) {}
//产品返回数据
export class ProductRes extends SuccessWrapper(Product) {}
export class ProductsRes extends SuccessArrayWrapper(Product) {}
//产品分类返分页数据
export class CategoryPaginatedResponse extends PaginatedWrapper(Category) {}
export class FlavorsPaginatedResponse extends PaginatedWrapper(Flavors) {}
export class StrengthPaginatedResponse extends PaginatedWrapper(Strength) {}
//产品分类返分页返回数据
export class ProductCatListRes extends SuccessWrapper(
CategoryPaginatedResponse
) {}
//产品分类返所有数据
export class ProductCatAllRes extends SuccessArrayWrapper(Category) {}
//产品分类返回数据
export class ProductCatRes extends SuccessWrapper(Category) {}
//产品分页数据
export class WpProductPaginatedResponse extends PaginatedWrapper(
WpProductDTO
) {}
//产品分页返回数据
export class WpProductListRes extends SuccessWrapper(
WpProductPaginatedResponse
) {}
export class LoginRes extends SuccessWrapper(LoginResDTO) {}
export class StockPaginatedRespone extends PaginatedWrapper(StockDTO) {}
export class StockListRes extends SuccessWrapper(StockPaginatedRespone) {}
export class StockRecordPaginatedRespone extends PaginatedWrapper(
StockRecordDTO
) {}
export class StockRecordListRes extends SuccessWrapper(
StockRecordPaginatedRespone
) {}
export class StockPointAllRespone extends SuccessArrayWrapper(StockPoint) {}
export class StockPointPaginatedRespone extends PaginatedWrapper(StockPoint) {}
export class StockPointListRes extends SuccessWrapper(
StockPointPaginatedRespone
) {}
export class PurchaseOrderPaginatedRespone extends PaginatedWrapper(
PurchaseOrderDTO
) {}
export class PurchaseOrderListRes extends SuccessWrapper(
PurchaseOrderPaginatedRespone
) {}
export class OrderStatusCountRes extends SuccessArrayWrapper(
OrderStatusCountDTO
) {}
export class OrderPaginatedRespone extends PaginatedWrapper(Order) {
@ApiProperty({ type: OrderStatusCountDTO, isArray: true })
count: OrderStatusCountDTO;
}
export class OrderListRes extends SuccessWrapper(OrderPaginatedRespone) {}
export class OrderSaleDTO extends OrderSale {
@ApiProperty()
totalQuantity: number;
}
export class OrderSalePaginatedRespone extends PaginatedWrapper(OrderSaleDTO) {}
export class OrderSaleListRes extends SuccessWrapper(
OrderSalePaginatedRespone
) {}
export class ServiceListRes extends SuccessArrayWrapper(Service) {}
export class RateLitRes extends SuccessArrayWrapper(RateDTO) {}
export class ShippingAddressListRes extends SuccessArrayWrapper(
ShippingAddress
) {}
export class OrderDetail extends Order {
@ApiProperty({ type: OrderItem, isArray: true })
items: OrderItem[];
@ApiProperty({ type: OrderSale, isArray: true })
sales: OrderSale[];
@ApiProperty({ type: OrderRefundItem, isArray: true })
refundItems: OrderRefundItem[];
// @ApiProperty({ type: Tracking, isArray: true })
// trackings: Tracking[];
@ApiProperty({ type: OrderNote, isArray: true })
notes: OrderNote[];
}
export class OrderDetailRes extends SuccessWrapper(OrderDetail) {}
export class PaymentMethodListRes extends SuccessArrayWrapper(
PaymentMethodDTO
) {}
// 订阅分页数据(列表 + 总数等分页信息)
export class SubscriptionPaginatedResponse extends PaginatedWrapper(Subscription) {}
// 订阅分页返回数据(统一成功包装)
export class SubscriptionListRes extends SuccessWrapper(SubscriptionPaginatedResponse) {}

82
src/dto/site.dto.ts Normal file
View File

@ -0,0 +1,82 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate';
export class SiteConfig {
@ApiProperty({ example: '1', description: '站点 ID' })
@Rule(RuleType.string())
id: string;
@ApiProperty({ description: '站点 URL' })
@Rule(RuleType.string())
apiUrl: string;
@ApiProperty({ description: '站点 rest key' })
@Rule(RuleType.string())
consumerKey: string;
@ApiProperty({ description: '站点 rest 秘钥' })
@Rule(RuleType.string())
consumerSecret: string;
@ApiProperty({ description: '站点名' })
@Rule(RuleType.string())
siteName: string;
@ApiProperty({ description: '平台类型', enum: ['woocommerce', 'shopyy'] })
@Rule(RuleType.string().valid('woocommerce', 'shopyy'))
type: string;
@ApiProperty({ description: 'SKU 前缀' })
@Rule(RuleType.string())
skuPrefix: string;
}
export class CreateSiteDTO {
@Rule(RuleType.string().optional())
apiUrl?: string;
@Rule(RuleType.string().optional())
consumerKey?: string;
@Rule(RuleType.string().optional())
consumerSecret?: string;
@Rule(RuleType.string())
siteName: string;
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
type?: string;
@Rule(RuleType.string().optional())
skuPrefix?: string;
}
export class UpdateSiteDTO {
@Rule(RuleType.string().optional())
apiUrl?: string;
@Rule(RuleType.string().optional())
consumerKey?: string;
@Rule(RuleType.string().optional())
consumerSecret?: string;
@Rule(RuleType.string().optional())
siteName?: string;
@Rule(RuleType.boolean().optional())
isDisabled?: boolean;
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
type?: string;
@Rule(RuleType.string().optional())
skuPrefix?: string;
}
export class QuerySiteDTO {
@Rule(RuleType.number().optional())
current?: number;
@Rule(RuleType.number().optional())
pageSize?: number;
@Rule(RuleType.string().optional())
keyword?: string;
@Rule(RuleType.boolean().optional())
isDisabled?: boolean;
@Rule(RuleType.string().optional())
ids?: string;
}
export class DisableSiteDTO {
@Rule(RuleType.boolean())
disabled: boolean;
}

36
src/dto/statistics.dto.ts Normal file
View File

@ -0,0 +1,36 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate';
import dayjs = require('dayjs');
export class OrderStatisticsParams {
@ApiProperty()
@Rule(RuleType.date().default(dayjs().subtract(1, 'month')))
startDate: Date;
@ApiProperty()
@Rule(RuleType.date().default(dayjs().subtract(1, 'month')))
endDate: Date;
@ApiProperty()
@Rule(RuleType.string().allow(null))
keyword?: string;
@ApiProperty()
@Rule(RuleType.number().allow(null))
siteId?: number;
@ApiProperty({
enum: ['all', 'first_purchase', 'repeat_purchase'],
default: 'all',
})
@Rule(RuleType.string().valid('all', 'first_purchase', 'repeat_purchase'))
purchaseType: string;
@ApiProperty({ enum: ['all', 'cpc', 'non_cpc'], default: 'all' })
@Rule(RuleType.string().valid('all', 'cpc', 'non_cpc'))
orderType: string;
@ApiProperty({ enum: ['all', 'zyn', 'yoone', 'zolt'], default: 'all' })
@Rule(RuleType.string().valid('all', 'zyn', 'yoone', 'zolt'))
brand: string;
}

185
src/dto/stock.dto.ts Normal file
View File

@ -0,0 +1,185 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate';
import { Stock } from '../entity/stock.entity';
import {
PurchaseOrderStatus,
StockRecordOperationType,
} from '../enums/base.enum';
import { PurchaseOrderItem } from '../entity/purchase_order_item.entity';
import { PurchaseOrder } from '../entity/purchase_order.entity';
import { StockRecord } from '../entity/stock_record.entity';
export class QueryStockDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number; // 页码
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number; // 每页大小
@ApiProperty()
@Rule(RuleType.string())
productName: string;
}
export class QueryPointDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number; // 页码
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number; // 每页大小
}
export class QueryStockRecordDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number; // 页码
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number; // 每页大小
@ApiProperty()
@Rule(RuleType.number())
stockPointId: number;
@ApiProperty()
@Rule(RuleType.string())
productSku: string;
@ApiProperty()
@Rule(RuleType.string())
productName: string;
@ApiProperty()
@Rule(RuleType.string())
operationType: string;
@ApiProperty()
@Rule(RuleType.date())
startDate: Date;
@ApiProperty()
@Rule(RuleType.date())
endDate: Date;
}
export class QueryPurchaseOrderDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number; // 页码
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number; // 每页大小
@ApiProperty()
@Rule(RuleType.string())
orderNumber?: string;
@ApiProperty()
@Rule(RuleType.number())
stockPointId?: number;
}
export class StockDTO extends Stock {
@ApiProperty()
@Rule(RuleType.string())
productName: string;
@ApiProperty({
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' },
quantity: { type: 'number' },
},
isArray: true,
})
@Rule(RuleType.array())
stockPoint: Array<{
id: number;
name: string;
quantity: number;
}>;
}
export class StockRecordDTO extends StockRecord {
@ApiProperty()
@Rule(RuleType.string())
productName: string;
}
export class PurchaseOrderDTO extends PurchaseOrder {
@ApiProperty({ type: PurchaseOrderItem, isArray: true })
@Rule(RuleType.array())
items: PurchaseOrderItem[];
}
export class UpdateStockDTO {
@ApiProperty()
@Rule(RuleType.number())
stockPointId: number;
@ApiProperty()
@Rule(RuleType.string())
productSku: string;
@ApiProperty()
@Rule(RuleType.number())
quantityChange: number;
@ApiProperty({ type: 'enum', enum: StockRecordOperationType })
@Rule(RuleType.string().valid(...Object.values(StockRecordOperationType)))
operationType: StockRecordOperationType;
@ApiProperty()
@Rule(RuleType.number())
operatorId: number;
@ApiProperty()
@Rule(RuleType.string().allow(''))
note: string;
}
export class CreateStockPointDTO {
@ApiProperty()
@Rule(RuleType.string())
name: string;
@ApiProperty()
@Rule(RuleType.string())
location: string;
@ApiProperty()
@Rule(RuleType.string())
contactPerson: string;
@ApiProperty()
@Rule(RuleType.string())
contactPhone: string;
}
export class UpdateStockPointDTO extends CreateStockPointDTO {}
export class CreatePurchaseOrderDTO {
@ApiProperty()
@Rule(RuleType.number())
stockPointId: number;
@ApiProperty()
@Rule(RuleType.date())
expectedArrivalTime: Date;
@ApiProperty({ type: 'enum', enum: PurchaseOrderStatus })
@Rule(RuleType.string().valid(...Object.values(PurchaseOrderStatus)))
status: PurchaseOrderStatus;
@ApiProperty()
@Rule(RuleType.string().allow(''))
note?: string;
@ApiProperty({
type: PurchaseOrderItem,
isArray: true,
})
@Rule(RuleType.array())
items: PurchaseOrderItem[];
}
export class UpdatePurchaseOrderDTO extends CreatePurchaseOrderDTO {}

View File

@ -0,0 +1,36 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate';
import { SubscriptionStatus } from '../enums/base.enum';
// 订阅列表查询参数(分页与筛选)
export class QuerySubscriptionDTO {
// 当前页码(从 1 开始)
@ApiProperty({ example: 1, description: '页码' })
@Rule(RuleType.number().default(1))
current: number;
// 每页数量
@ApiProperty({ example: 10, description: '每页大小' })
@Rule(RuleType.number().default(10))
pageSize: number;
// 站点 ID可选
@ApiProperty({ description: '站点ID' })
@Rule(RuleType.string().allow(''))
siteId: string;
// 订阅状态筛选(可选),支持枚举值
@ApiProperty({ description: '订阅状态', enum: SubscriptionStatus })
@Rule(RuleType.string().valid(...Object.values(SubscriptionStatus)).allow(''))
status: SubscriptionStatus | '';
// 客户邮箱(模糊匹配,可选)
@ApiProperty({ description: '客户邮箱' })
@Rule(RuleType.string().allow(''))
customer_email: string;
// 关键字订阅ID、邮箱等模糊匹配可选
@ApiProperty({ description: '关键字订阅ID、邮箱等' })
@Rule(RuleType.string().allow(''))
keyword: string;
}

26
src/dto/user.dto.ts Normal file
View File

@ -0,0 +1,26 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate';
export class LoginResDTO {
@ApiProperty({ type: String })
token: string;
@ApiProperty({ type: Number })
userId: number;
@ApiProperty({ type: String })
username: string;
@ApiProperty({ type: String, isArray: true })
permissions: string[];
}
export class LoginDTO {
@ApiProperty({ type: String })
@Rule(RuleType.string())
username: string;
@ApiProperty({ type: String })
@Rule(RuleType.string())
password: string;
}

98
src/dto/wp_product.dto.ts Normal file
View File

@ -0,0 +1,98 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Variation } from '../entity/variation.entity';
import { WpProduct } from '../entity/wp_product.entity';
import { Rule, RuleType } from '@midwayjs/validate';
import { ProductStatus } from '../enums/base.enum';
export class VariationDTO extends Variation {}
export class WpProductDTO extends WpProduct {
@ApiProperty({ description: '变体列表', type: VariationDTO, isArray: true })
variations?: VariationDTO[];
}
export class UpdateVariationDTO {
@ApiProperty({ description: '产品名称' })
@Rule(RuleType.string())
name: string;
@ApiProperty({ description: 'SKU' })
@Rule(RuleType.string().allow(''))
sku: string;
@ApiProperty({ description: '常规价格', type: Number })
@Rule(RuleType.number())
regular_price: number; // 常规价格
@ApiProperty({ description: '销售价格', type: Number })
@Rule(RuleType.number())
sale_price: number; // 销售价格
@ApiProperty({ description: '是否促销中', type: Boolean })
@Rule(RuleType.boolean())
on_sale: boolean; // 是否促销中
}
export class UpdateWpProductDTO {
@ApiProperty({ description: '变体名称' })
@Rule(RuleType.string())
name: string;
@ApiProperty({ description: 'SKU' })
@Rule(RuleType.string().allow(''))
sku: string;
@ApiProperty({ description: '常规价格', type: Number })
@Rule(RuleType.number())
regular_price: number; // 常规价格
@ApiProperty({ description: '销售价格', type: Number })
@Rule(RuleType.number())
sale_price: number; // 销售价格
@ApiProperty({ description: '是否促销中', type: Boolean })
@Rule(RuleType.boolean())
on_sale: boolean; // 是否促销中
}
export class QueryWpProductDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number;
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number;
@ApiProperty({ example: 'ZYN', description: '产品名' })
@Rule(RuleType.string())
name?: string;
@ApiProperty({ example: '1', description: '站点ID' })
@Rule(RuleType.string())
siteId?: string;
@ApiProperty({ description: '产品状态', enum: ProductStatus })
@Rule(RuleType.string().valid(...Object.values(ProductStatus)))
status?: ProductStatus;
}
export class SetConstitutionDTO {
@ApiProperty({ type: Boolean })
@Rule(RuleType.boolean())
isProduct: boolean;
@ApiProperty({
description: '构成成分',
type: 'array',
items: {
type: 'object',
properties: {
sku: { type: 'string' },
quantity: { type: 'number' },
},
},
})
@Rule(RuleType.array())
constitution: { sku: string; quantity: number }[] | null;
}

13
src/entity/auth_code.ts Normal file
View File

@ -0,0 +1,13 @@
import { Entity, Column, PrimaryColumn } from 'typeorm';
@Entity('auth_code')
export class AuthCode {
@PrimaryColumn({ length: 255 })
device_id: string;
@Column({ length: 10 })
code: string;
@Column()
expires_at: Date;
}

View File

@ -0,0 +1,53 @@
import {
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Entity,
} from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger';
@Entity()
export class Category {
@ApiProperty({
example: '1',
description: '分类 ID',
type: 'number',
required: true,
})
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({
example: '分类名称',
description: '分类名称',
type: 'string',
required: true,
})
@Column()
name: string;
@ApiProperty({
description: '唯一识别key',
type: 'string',
required: true,
})
@Column()
unique_key: string;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,13 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('customer')
export class Customer {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string;
@Column({ default: 0})
rate: number;
}

View File

@ -0,0 +1,25 @@
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('customer_tag')
export class CustomerTag {
@PrimaryGeneratedColumn()
id: number;
@Column()
email: string;
@Column()
tag: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,14 @@
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity('device_whitelist')
export class DeviceWhitelist {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'device_id', unique: true })
deviceId: string;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
}

View File

@ -0,0 +1,43 @@
import {
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Entity,
} from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger';
@Entity()
export class Flavors {
@ApiProperty()
@PrimaryGeneratedColumn()
id: number;
@ApiProperty()
@Column()
name: string;
@ApiProperty({
description: '唯一识别key',
type: 'string',
required: true,
})
@Column()
unique_key: string;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
}

290
src/entity/order.entity.ts Normal file
View File

@ -0,0 +1,290 @@
import { ApiProperty } from '@midwayjs/swagger';
import {
BeforeInsert,
BeforeUpdate,
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
OneToOne,
JoinColumn
} from 'typeorm';
import { ErpOrderStatus, OrderStatus } from '../enums/base.enum';
import { OrderAddress } from '../dto/order.dto';
import { Exclude, Expose } from 'class-transformer';
import { Shipment } from './shipment.entity';
@Entity('order')
@Exclude()
export class Order {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id: number;
@ApiProperty()
@Column()
@Expose()
siteId: string; // 来源站点唯一标识
@ApiProperty()
@Column()
@Expose()
externalOrderId: string; // WooCommerce 订单 ID
@ApiProperty({ type: OrderStatus })
@Column({ type: 'enum', enum: OrderStatus })
@Expose()
status: OrderStatus; // WooCommerce 订单 状态
@ApiProperty({ type: ErpOrderStatus })
@Column({
type: 'enum',
enum: ErpOrderStatus,
default: ErpOrderStatus.PENDING,
})
@Expose()
orderStatus: ErpOrderStatus; // 订单状态
@ApiProperty()
@Column({ name: 'shipment_id', nullable: true })
@Expose()
shipmentId: number;
@OneToOne(() => Shipment)
@JoinColumn({ name: 'shipment_id' })
shipment: Shipment;
@ApiProperty()
@Column()
@Expose()
currency: string;
@ApiProperty()
@Column()
@Expose()
currency_symbol: string;
@ApiProperty()
@Column({ default: false })
@Expose()
prices_include_tax: boolean;
@ApiProperty()
@Column({ type: 'timestamp', nullable: true })
@Expose()
date_created: Date;
@ApiProperty()
@Column({ type: 'timestamp', nullable: true })
@Expose()
date_modified: Date;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, default: 0 })
@Expose()
discount_total: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, default: 0 })
@Expose()
discount_tax: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, default: 0 })
@Expose()
shipping_total: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, default: 0 })
@Expose()
shipping_tax: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, default: 0 })
@Expose()
cart_tax: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, default: 0 })
@Expose()
total: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, default: 0 })
@Expose()
total_tax: number;
@ApiProperty()
@Column({ default: 0 })
@Expose()
customer_id: number;
@ApiProperty()
@Column()
@Expose()
customer_email: string;
@ApiProperty()
@Column({ default: '' })
@Expose()
order_key: string;
@ApiProperty({ type: OrderAddress })
@Column({ type: 'json', nullable: true })
@Expose()
billing: OrderAddress;
@ApiProperty({ type: OrderAddress })
@Column({ type: 'json', nullable: true })
@Expose()
shipping: OrderAddress;
@ApiProperty()
@Column({ default: '' })
@Expose()
payment_method: string;
@ApiProperty()
@Column({ default: '' })
@Expose()
payment_method_title: string;
@ApiProperty()
@Column({ default: '' })
@Expose()
transaction_id: string;
@ApiProperty()
@Column({ default: '' })
@Expose()
customer_ip_address: string;
@ApiProperty()
@Column({
type: 'varchar',
length: 1024,
nullable: true,
})
@Expose()
customer_user_agent: string;
@ApiProperty()
@Column({ default: '' })
@Expose()
created_via: string;
@ApiProperty()
@Column({
type: 'mediumtext', // 设置字段类型为 MEDIUMTEXT
nullable: true, // 可选:是否允许为 NULL
})
@Expose()
customer_note: string;
@ApiProperty()
@Column({ type: 'timestamp', nullable: true })
@Expose()
date_completed: Date;
@ApiProperty()
@Column({ type: 'timestamp', nullable: true })
@Expose()
date_paid: Date;
@ApiProperty()
@Column({ default: '' })
@Expose()
cart_hash: string;
@ApiProperty()
@Column({ default: '' })
@Expose()
number: string;
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
meta_data: any[];
@ApiProperty()
@Column({ default: '' })
@Expose()
payment_url: string;
@ApiProperty()
@Column({ default: false })
@Expose()
is_editable: boolean;
@ApiProperty()
@Column({ default: false })
@Expose()
needs_payment: boolean;
@ApiProperty()
@Column({ default: false })
@Expose()
needs_processing: boolean;
@ApiProperty()
@Column({ default: '' })
@Expose()
device_type: string;
@ApiProperty()
@Column({ default: '' })
@Expose()
source_type: string;
@ApiProperty()
@Column({ default: '' })
@Expose()
utm_source: string;
@ApiProperty()
@Column({ default: false })
@Expose()
is_exchange: boolean;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 0, default: 0 })
@Expose()
exchange_frequency: number;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt: Date;
// 在插入或更新前处理用户代理字符串
@BeforeInsert()
@BeforeUpdate()
truncateUserAgent() {
const maxLength = 1024; // 根据数据库限制的实际长度
if (
this.customer_user_agent &&
this.customer_user_agent.length > maxLength
) {
this.customer_user_agent = this.customer_user_agent.substring(
0,
maxLength
);
}
}
}

View File

@ -0,0 +1,86 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('order_coupon')
@Exclude()
export class OrderCoupon {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id: number;
@ApiProperty()
@Column()
@Expose()
orderId: number; // 订单 ID
@ApiProperty()
@Column()
@Expose()
siteId: string; // 来源站点唯一标识
@ApiProperty()
@Column()
@Expose()
externalOrderId: string; // WooCommerce 订单 ID
@ApiProperty()
@Column()
@Expose()
externalOrderCouponId: string; // WooCommerce 订单coupon ID
@ApiProperty()
@Column()
@Expose()
code: string;
@ApiProperty()
@Column()
@Expose()
discount: string;
@ApiProperty()
@Column()
@Expose()
discount_tax: string;
@ApiProperty()
@Column()
@Expose()
discount_type: string;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
nominal_amount: number;
@ApiProperty()
@Column()
@Expose()
free_shipping: boolean;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt: Date;
}

View File

@ -0,0 +1,86 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('order_fee')
@Exclude()
export class OrderFee {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id?: number;
@ApiProperty()
@Column()
@Expose()
orderId: number; // 订单 ID
@ApiProperty()
@Column()
@Expose()
siteId: string;
@ApiProperty()
@Column()
@Expose()
externalOrderId: string; // WooCommerce 订单 ID
@ApiProperty()
@Column()
@Expose()
externalOrderFeeId: string; // WooCommerce 订单fee ID
@ApiProperty()
@Column()
@Expose()
name: string;
@ApiProperty()
@Column()
@Expose()
tax_class: string;
@ApiProperty()
@Column()
@Expose()
tax_status: string;
@ApiProperty()
@Column()
@Expose()
amount: string;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
total: string;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
total_tax: string;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt?: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt?: Date;
}

View File

@ -0,0 +1,151 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('order_item')
@Exclude()
export class OrderItem {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id: number;
@ApiProperty()
@Column()
@Expose()
name: string;
@ApiProperty()
@Column()
@Expose()
siteId: string; // 来源站点唯一标识
@ApiProperty()
@Column()
@Expose()
orderId: number; // 订单 ID
@ApiProperty()
@Column()
@Expose()
externalOrderId: string; // WooCommerce 订单 ID
@ApiProperty()
@Column({ nullable: true })
@Expose()
externalOrderItemId: string; // WooCommerce 订单item ID
@ApiProperty()
@Column()
@Expose()
externalProductId: string; // WooCommerce 产品 ID
@ApiProperty()
@Column()
@Expose()
externalVariationId: string; // WooCommerce 变体 ID
@ApiProperty()
@Column()
@Expose()
quantity: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, nullable: true })
@Expose()
subtotal: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, nullable: true })
@Expose()
subtotal_tax: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, nullable: true })
@Expose()
total: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, nullable: true })
@Expose()
total_tax: number;
@ApiProperty()
@Column({ nullable: true })
@Expose()
tax_class?: string; // 税类(来自 line_items.tax_class
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
taxes?: any[]; // 税明细(来自 line_items.taxes数组
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
meta_data?: any[]; // 行项目元数据(包含订阅相关键值)
@ApiProperty()
@Column({ nullable: true })
@Expose()
sku?: string;
@ApiProperty()
@Column({ nullable: true })
@Expose()
global_unique_id?: string; // 全局唯一ID部分主题/插件会提供)
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
price: number;
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
image?: { id?: string | number; src?: string }; // 商品图片(对象,包含 id/src
@ApiProperty()
@Column({ nullable: true })
@Expose()
parent_name?: string; // 父商品名称(组合/捆绑时可能使用)
@ApiProperty()
@Column({ nullable: true })
@Expose()
bundled_by?: string; // 捆绑来源标识bundled_by
@ApiProperty()
@Column({ nullable: true })
@Expose()
bundled_item_title?: string; // 捆绑项标题
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
bundled_items?: any[]; // 捆绑项列表(数组)
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt: Date;
}

View File

@ -0,0 +1,82 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { Order } from './order.entity';
@Entity('order_sale_original')
@Exclude()
export class OrderSaleOriginal {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id?: number;
@ApiProperty()
@ManyToOne(() => Order)
@JoinColumn({ name: 'order_id' })
@Column({ name: 'order_id' })
@Expose()
orderId: number; // 订单 ID
@ApiProperty()
@Column()
@Expose()
siteId: string; // 来源站点唯一标识
@ApiProperty()
@Column({ nullable: true })
@Expose()
externalOrderItemId: string; // WooCommerce 订单item ID
@ApiProperty()
@Column()
@Expose()
productId: number;
@ApiProperty()
@Column()
@Expose()
name: string;
@ApiProperty({ description: 'sku', type: 'string' })
@Expose()
@Column()
sku: string;
@ApiProperty()
@Column()
@Expose()
quantity: number;
@ApiProperty()
@Column({ default: false })
@Expose()
isPackage: boolean;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
})
@CreateDateColumn()
@Expose()
createdAt?: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
})
@UpdateDateColumn()
@Expose()
updatedAt?: Date;
}

View File

@ -0,0 +1,113 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { Order } from './order.entity';
@Entity('order_item_original')
@Exclude()
export class OrderItemOriginal {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id: number;
@ApiProperty()
@ManyToOne(() => Order)
@JoinColumn({ name: 'order_id' })
@Column({ name: 'order_id' })
@Expose()
orderId: number; // 订单 ID
@ApiProperty()
@Column()
@Expose()
name: string;
@ApiProperty()
@Column()
@Expose()
siteId: string; // 来源站点唯一标识
@ApiProperty()
@Column()
@Expose()
externalOrderId: string; // WooCommerce 订单 ID
@ApiProperty()
@Column({ nullable: true })
@Expose()
externalOrderItemId: string; // WooCommerce 订单item ID
@ApiProperty()
@Column()
@Expose()
externalProductId: string; // WooCommerce 产品 ID
@ApiProperty()
@Column()
@Expose()
externalVariationId: string; // WooCommerce 变体 ID
@ApiProperty()
@Column()
@Expose()
quantity: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, nullable: true })
@Expose()
subtotal: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, nullable: true })
@Expose()
subtotal_tax: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, nullable: true })
@Expose()
total: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, nullable: true })
@Expose()
total_tax: number;
@ApiProperty()
@Column({ nullable: true })
@Expose()
sku?: string;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
price: number;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt: Date;
}

View File

@ -0,0 +1,49 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('order_note')
@Exclude()
export class OrderNote {
@PrimaryGeneratedColumn()
id: number;
@ApiProperty()
@Column()
@Expose()
userId: number;
@ApiProperty()
@Column()
@Expose()
orderId: number; // 订单 ID
@ApiProperty()
@Column()
@Expose()
content: string;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt: Date;
}

View File

@ -0,0 +1,86 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('order_refund')
@Exclude()
export class OrderRefund {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id: number;
@ApiProperty()
@Column()
@Expose()
orderId: number; // 订单 ID
@ApiProperty()
@Column()
@Expose()
siteId: string; // 来源站点唯一标识
@ApiProperty()
@Column()
@Expose()
externalOrderId: string; // WooCommerce 订单 ID
@ApiProperty()
@Column()
@Expose()
externalRefundId: string; // WooCommerce refund ID
@ApiProperty()
@Column({ type: 'timestamp' })
@Expose()
date_created: Date;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
amount: number;
@ApiProperty()
@Column()
@Expose()
reason: string;
@ApiProperty()
@Column()
@Expose()
refunded_by: number;
@ApiProperty()
@Column()
@Expose()
refunded_payment: boolean;
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
meta_data: [];
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt: Date;
}

View File

@ -0,0 +1,111 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('order_refund_item')
@Exclude()
export class OrderRefundItem {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id?: number;
@ApiProperty()
@Column()
@Expose()
refundId: number; // 订单 refund ID
@ApiProperty()
@Column()
@Expose()
siteId: string; // 来源站点唯一标识
@ApiProperty()
@Column()
@Expose()
externalRefundId: string; // WooCommerce refund ID
@ApiProperty()
@Column()
@Expose()
externalRefundItemId: string; // WooCommerce refund item ID
@ApiProperty()
@Column()
@Expose()
externalProductId: string; // WooCommerce 产品 ID
@ApiProperty()
@Column()
@Expose()
externalVariationId: string; // WooCommerce 变体 ID
@ApiProperty()
@Column()
@Expose()
name: string;
@ApiProperty()
@Column()
@Expose()
quantity: number;
@ApiProperty()
@Column()
@Expose()
tax_class: string;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
subtotal: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
subtotal_tax: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
total: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
total_tax: number;
@ApiProperty()
@Column({ nullable: true })
@Expose()
sku?: string;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
price: number;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt?: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt?: Date;
}

View File

@ -0,0 +1,117 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
BeforeInsert,
BeforeUpdate,
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('order_sale')
@Exclude()
export class OrderSale {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id?: number;
@ApiProperty()
@Column()
@Expose()
orderId: number; // 订单 ID
@ApiProperty()
@Column()
@Expose()
siteId: string; // 来源站点唯一标识
@ApiProperty()
@Column({ nullable: true })
@Expose()
externalOrderItemId: string; // WooCommerce 订单item ID
@ApiProperty()
@Column()
@Expose()
productId: number;
@ApiProperty()
@Column()
@Expose()
name: string;
@ApiProperty({ description: 'sku', type: 'string' })
@Expose()
@Column()
sku: string;
@ApiProperty()
@Column()
@Expose()
quantity: number;
@ApiProperty()
@Column({ default: false })
@Expose()
isPackage: boolean;
@ApiProperty()
@Column({ default: false })
@Expose()
isYoone: boolean;
@ApiProperty()
@Column({ default: false })
@Expose()
isZex: boolean;
@ApiProperty({ nullable: true })
@Column({ type: 'int', nullable: true })
@Expose()
size: number | null;
@ApiProperty()
@Column({ default: false })
@Expose()
isYooneNew: boolean;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
})
@CreateDateColumn()
@Expose()
createdAt?: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
})
@UpdateDateColumn()
@Expose()
updatedAt?: Date;
// === 自动计算逻辑 ===
@BeforeInsert()
@BeforeUpdate()
setFlags() {
if (!this.name) return;
const lower = this.name.toLowerCase();
this.isYoone = lower.includes('yoone');
this.isZex = lower.includes('zex');
this.isYooneNew = this.isYoone && lower.includes('new');
let size: number | null = null;
const sizes = [3, 6, 9, 12, 15, 18];
for (const s of sizes) {
if (lower.includes(s.toString())) {
size = s;
break;
}
}
this.size = size;
}
}

View File

@ -0,0 +1,21 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('order_shipment')
export class OrderShipment {
@ApiProperty()
@PrimaryGeneratedColumn()
id: number;
@ApiProperty()
@Column()
order_id: number;
@ApiProperty()
@Column()
shipment_id: number;
@ApiProperty()
@Column()
stockPointId: number;
}

View File

@ -0,0 +1,81 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('order_shipping')
@Exclude()
export class OrderShipping {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id: number;
@ApiProperty()
@Column()
@Expose()
orderId: number; // 订单 ID
@ApiProperty()
@Column()
@Expose()
siteId: string;
@ApiProperty()
@Column()
@Expose()
externalOrderId: string; // WooCommerce 订单 ID
@ApiProperty()
@Column()
@Expose()
externalOrderShippingId: string; // WooCommerce 订单快递 ID
@ApiProperty()
@Column()
@Expose()
method_title: string;
@ApiProperty()
@Column()
@Expose()
method_id: string;
@ApiProperty()
@Column()
@Expose()
instance_id: string;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
total: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
total_tax: number;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt: Date;
}

View File

@ -0,0 +1,73 @@
import {
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Entity,
} from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger';
@Entity()
export class Product {
@ApiProperty({
example: '1',
description: 'ID',
type: 'number',
required: true,
})
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({
example: 'ZYN 6MG WINTERGREEN',
description: '产品名称',
type: 'string',
required: true,
})
@Column()
name: string;
@ApiProperty()
@Column({ default: ''})
nameCn: string;
@ApiProperty({ example: '产品描述', description: '产品描述', type: 'string' })
@Column({ nullable: true })
description?: string;
@ApiProperty({ example: '1', description: '分类 ID', type: 'number' })
@Column()
categoryId: number;
@ApiProperty()
@Column()
flavorsId: number;
@ApiProperty()
@Column()
strengthId: number;
@ApiProperty()
@Column()
humidity: string;
@ApiProperty({ description: 'sku', type: 'string' })
@Column({ nullable: true })
sku?: string;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,61 @@
// src/entity/PurchaseOrder.ts
import { ApiProperty } from '@midwayjs/swagger';
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { PurchaseOrderStatus } from '../enums/base.enum';
@Entity('purchase_order')
export class PurchaseOrder {
@ApiProperty({ type: 'number' })
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({ type: Number })
@Column()
stockPointId: number;
@ApiProperty({ type: 'string' })
@Column()
orderNumber: string;
@ApiProperty({ type: PurchaseOrderStatus })
@Column({
type: 'enum',
enum: PurchaseOrderStatus,
default: 'draft',
})
status: PurchaseOrderStatus;
@ApiProperty()
@Column({ nullable: true, type: 'text' })
note: string;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '预计时间',
required: true,
})
@Column({ type: 'timestamp', nullable: true })
expectedArrivalTime: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,30 @@
// src/entity/PurchaseOrderItem.ts
import { ApiProperty } from '@midwayjs/swagger';
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('purchase_order_item')
export class PurchaseOrderItem {
@ApiProperty({ type: Number })
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({ type: String })
@Column()
productSku: string;
@ApiProperty({ type: String })
@Column()
productName: string;
@ApiProperty({ type: Number })
@Column()
quantity: number;
@ApiProperty({ type: Number })
@Column({ type: 'decimal', precision: 10, scale: 2 })
price: number;
@ApiProperty({ type: Number })
@Column()
purchaseOrderId: number;
}

View File

@ -0,0 +1,51 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity()
@Exclude()
export class Service {
@ApiProperty()
@PrimaryColumn()
@Expose()
id?: string;
@ApiProperty()
@Column()
@Expose()
carrier_name: string;
@ApiProperty()
@Column()
@Expose()
service_name: string;
@ApiProperty()
@Column({ default: false })
@Expose()
isActive: boolean;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt?: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt?: Date;
}

View File

@ -0,0 +1,140 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
ManyToOne,
OneToOne,
JoinColumn
} from 'typeorm';
import { StockPoint } from './stock_point.entity';
import { Order } from './order.entity';
@Entity('shipment')
@Exclude()
export class Shipment {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id: number;
@ApiProperty()
@Column({ nullable: true })
order_id: number;
@ApiProperty()
@OneToOne(() => Order)
@JoinColumn({ name: 'order_id' })
order: Order;
@ApiProperty()
@Column({ name: 'stock_point_id' })
@Expose()
stockPointId: string;
@ManyToOne(() => StockPoint)
@JoinColumn({ name: 'stock_point_id' })
stockPoint: StockPoint;
@Column({ nullable: false, default: 0})
@Expose()
fee: number;
@ApiProperty()
@Column({ nullable: true })
@Expose()
tracking_id: string;
@ApiProperty()
@Column({ nullable: true })
@Expose()
tracking_provider?: string;
@ApiProperty()
@Column()
@Expose()
unique_id: string;
@Column({ nullable: true })
@Expose()
state?: string;
@Column({ nullable: false, default: false })
@Expose()
finished: boolean;
@Column({ nullable: true })
@Expose()
type?: string;
@ApiProperty()
@Column({ nullable: true })
@Expose()
transaction_number?: string;
@ApiProperty()
@Column({ nullable: true })
@Expose()
primary_tracking_number?: string;
@ApiProperty({ type: [String] })
@Column({ type: 'json', nullable: true })
@Expose()
tracking_numbers?: string[];
@ApiProperty()
@Column({ nullable: true })
@Expose()
tracking_url?: string;
@ApiProperty()
@Column({ nullable: true })
@Expose()
return_tracking_number?: string;
@ApiProperty()
@Column({ nullable: true })
@Expose()
bol_number?: string;
@ApiProperty()
@Column({ nullable: true })
@Expose()
pickup_confirmation_number?: string;
@ApiProperty()
@Column({ nullable: true })
@Expose()
customs_invoice_url?: string;
@ApiProperty({ type: Object })
@Column({ type: 'json', nullable: true })
@Expose()
rate?: Record<string, any>;
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
labels?: Array<any>;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt?: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt?: Date;
}

View File

@ -0,0 +1,67 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { Shipment } from './shipment.entity';
import { Order } from './order.entity';
@Entity('shipment_item')
@Exclude()
export class ShipmentItem {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id: number;
@ApiProperty()
@Expose()
@ManyToOne(() => Shipment)
@JoinColumn({ name: 'shipment_id' })
shipment_id: number;
@ApiProperty()
@Expose()
@ManyToOne(() => Order)
@JoinColumn({ name: 'order_id' })
order_id: number;
@ApiProperty()
@Column({ nullable: true })
@Expose()
name: string;
@ApiProperty({ description: 'sku', type: 'string' })
@Expose()
@Column()
sku: string;
@ApiProperty()
@Column()
@Expose()
quantity: number;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt: Date;
}

View File

@ -0,0 +1,67 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { Address } from '../dto/freightcom.dto';
@Entity()
@Exclude()
export class ShippingAddress {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id?: number;
@ApiProperty()
@Column()
@Expose()
name: string;
@ApiProperty()
@Column()
@Expose()
stockPointId: number;
@ApiProperty({ type: Address })
@Column({ type: 'json', nullable: true })
@Expose()
address: Address;
@ApiProperty()
@Column()
@Expose()
phone_number: string;
@ApiProperty()
@Column()
@Expose()
phone_number_extension: string;
@ApiProperty()
@Column()
@Expose()
phone_number_country: string;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt?: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt?: Date;
}

28
src/entity/site.entity.ts Normal file
View File

@ -0,0 +1,28 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('site')
export class Site {
@PrimaryGeneratedColumn({ type: 'int' })
id: number;
@Column({ type: 'varchar', length: 255, nullable: true })
apiUrl: string;
@Column({ type: 'varchar', length: 255, nullable: true })
consumerKey: string;
@Column({ type: 'varchar', length: 255, nullable: true })
consumerSecret: string;
@Column({ type: 'varchar', length: 255, unique: true })
siteName: string;
@Column({ type: 'varchar', length: 32, default: 'woocommerce' })
type: string; // 平台类型woocommerce | shopyy
@Column({ type: 'varchar', length: 64, nullable: true })
skuPrefix: string;
@Column({ type: 'tinyint', default: 0 })
isDisabled: number;
}

View File

@ -0,0 +1,44 @@
// src/entity/Stock.ts
import { ApiProperty } from '@midwayjs/swagger';
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('stock')
export class Stock {
@ApiProperty({ type: Number })
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({ type: Number })
@Column()
stockPointId: number;
@ApiProperty({ type: String })
@Column()
productSku: string;
@ApiProperty({ type: Number })
@Column()
quantity: number;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,75 @@
import { ApiProperty } from '@midwayjs/swagger';
import {
BaseEntity,
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
OneToMany,
} from 'typeorm';
import { Shipment } from './shipment.entity';
@Entity('stock_point')
export class StockPoint extends BaseEntity {
@ApiProperty({ type: 'number' })
@PrimaryGeneratedColumn()
id: number;
@OneToMany(() => Shipment, shipment => shipment.stockPoint)
shipments: Shipment[];
@ApiProperty({ type: 'string' })
@Column()
name: string;
@ApiProperty({ type: 'string' })
@Column()
location: string;
@ApiProperty({ type: 'string' })
@Column()
contactPerson: string;
@ApiProperty({ type: 'string' })
@Column()
contactPhone: string;
@ApiProperty()
@Column({ default: false })
ignore: boolean;
@ApiProperty()
@Column({ default: false })
inCanada: boolean;
@ApiProperty()
@Column({ default: false })
isB: boolean;
@Column({ default: 'uniuni' })
upStreamName: string;
@Column()
upStreamStockPointId: number;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
@DeleteDateColumn()
deletedAt: Date; // 软删除时间
}

View File

@ -0,0 +1,48 @@
// src/entity/StockRecord.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
} from 'typeorm';
import { StockRecordOperationType } from '../enums/base.enum';
import { ApiProperty } from '@midwayjs/swagger';
@Entity('stock_record')
export class StockRecord {
@ApiProperty({ type: Number })
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({ type: Number })
@Column()
stockPointId: number;
@ApiProperty({ type: String })
@Column()
productSku: string;
@ApiProperty({ type: StockRecordOperationType })
@Column({ type: 'enum', enum: StockRecordOperationType })
operationType: StockRecordOperationType;
@ApiProperty({ type: Number })
@Column()
quantityChange: number;
@ApiProperty()
@Column()
operatorId: number;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({ type: String })
@Column({ nullable: true })
note: string;
}

View File

@ -0,0 +1,43 @@
import {
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Entity,
} from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger';
@Entity()
export class Strength {
@ApiProperty()
@PrimaryGeneratedColumn()
id: number;
@ApiProperty()
@Column()
name: string;
@ApiProperty({
description: '唯一识别key',
type: 'string',
required: true,
})
@Column()
unique_key: string;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,128 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { SubscriptionStatus } from '../enums/base.enum';
@Entity('subscription')
@Exclude()
export class Subscription {
// 本地主键,自增 ID
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id: number;
// 站点唯一标识,用于区分不同来源站点
@ApiProperty({ description: '来源站点唯一标识' })
@Column()
@Expose()
siteId: string;
// WooCommerce 订阅的原始 ID字符串化用于幂等更新
@ApiProperty({ description: 'WooCommerce 订阅 ID' })
@Column()
@Expose()
externalSubscriptionId: string;
// 订阅状态active/cancelled/on-hold 等)
@ApiProperty({ type: SubscriptionStatus })
@Column({ type: 'enum', enum: SubscriptionStatus })
@Expose()
status: SubscriptionStatus;
// 货币代码,例如 USD/CAD
@ApiProperty()
@Column({ default: '' })
@Expose()
currency: string;
// 总金额,保留两位小数
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, default: 0 })
@Expose()
total: number;
// 计费周期day/week/month/year
@ApiProperty({ description: '计费周期 e.g. day/week/month/year' })
@Column({ default: '' })
@Expose()
billing_period: string;
// 计费周期间隔(例如 1/3/12
@ApiProperty({ description: '计费周期间隔 e.g. 1/3/12' })
@Column({ type: 'int', default: 0 })
@Expose()
billing_interval: number;
// 客户 IDWooCommerce 用户 ID
@ApiProperty()
@Column({ type: 'int', default: 0 })
@Expose()
customer_id: number;
// 客户邮箱(从 billing.email 或 customer_email 提取)
@ApiProperty()
@Column({ default: '' })
@Expose()
customer_email: string;
// 父订单/订阅 ID如有
@ApiProperty({ description: '父订单/父订阅ID如有' })
@Column({ type: 'int', default: 0 })
@Expose()
parent_id: number;
// 订阅开始时间
@ApiProperty()
@Column({ type: 'timestamp', nullable: true })
@Expose()
start_date: Date;
// 试用结束时间
@ApiProperty()
@Column({ type: 'timestamp', nullable: true })
@Expose()
trial_end: Date;
// 下次支付时间
@ApiProperty()
@Column({ type: 'timestamp', nullable: true })
@Expose()
next_payment_date: Date;
// 订阅结束时间
@ApiProperty()
@Column({ type: 'timestamp', nullable: true })
@Expose()
end_date: Date;
// 商品项(订阅行项目)
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
line_items: any[];
// 额外元数据(键值对)
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
meta_data: any[];
// 创建时间(数据库自动生成)
@ApiProperty({ example: '2022-12-12 11:11:11', description: '创建时间', required: true })
@CreateDateColumn()
@Expose()
createdAt: Date;
// 更新时间(数据库自动生成)
@ApiProperty({ example: '2022-12-12 11:11:11', description: '更新时间', required: true })
@UpdateDateColumn()
@Expose()
updatedAt: Date;
}

View File

@ -0,0 +1,67 @@
import { ApiProperty } from '@midwayjs/swagger';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity()
export class Transfer {
@ApiProperty()
@PrimaryGeneratedColumn()
id?: number;
@ApiProperty({ type: 'string' })
@Column()
orderNumber: string;
@ApiProperty({ type: Number })
@Column()
sourceStockPointId: number;
@ApiProperty({ type: Number })
@Column()
destStockPointId: number;
@ApiProperty({})
@Column({ default: false })
isCancel: boolean;
@ApiProperty({})
@Column({ default: false })
isArrived: boolean;
@ApiProperty({})
@Column({ default: false })
isLost: boolean;
@ApiProperty()
@Column({ nullable: true, type: 'text' })
note: string;
@ApiProperty()
@Column({ nullable: true, type: Date })
sendAt: Date;
@ApiProperty()
@Column({ nullable: true, type: Date })
arriveAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,25 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('transfer_item')
export class TransferItem {
@ApiProperty({ type: Number })
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({ type: String })
@Column()
productSku: string;
@ApiProperty({ type: String })
@Column()
productName: string;
@ApiProperty({ type: Number })
@Column()
quantity: number;
@ApiProperty({ type: Number })
@Column()
transferId: number;
}

31
src/entity/user.entity.ts Normal file
View File

@ -0,0 +1,31 @@
// src/entity/user.entity.ts
import { Exclude } from 'class-transformer';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('user')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
username: string;
@Column()
@Exclude()
password: string;
// @Column() // 默认角色为管理员
// roleId: number; // 角色 (如admin, editor, viewer)
@Column({ type: 'simple-array', nullable: true })
permissions: string[]; // 自定义权限 (如:['user:add', 'user:edit'])
@Column({ default: false })
isSuper: boolean; // 超级管理员
@Column({ default: false })
isAdmin: boolean; // 管理员
@Column({ default: true })
isActive: boolean; // 用户是否启用
}

View File

@ -0,0 +1,119 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
Unique,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger';
@Entity('variation')
@Unique(['siteId', 'externalProductId', 'externalVariationId']) // 确保变体的唯一性
export class Variation {
@ApiProperty({
example: '1',
description: 'ID',
type: 'number',
required: true,
})
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({
example: '1',
description: 'wp网站ID',
type: 'string',
required: true,
})
@Column()
siteId: string; // 来源站点唯一标识
@ApiProperty({
example: '1',
description: 'wp产品ID',
type: 'string',
required: true,
})
@Column()
externalProductId: string; // WooCommerce 产品 ID
@ApiProperty({
example: '1',
description: 'wp变体ID',
type: 'string',
required: true,
})
@Column()
externalVariationId: string; // WooCommerce 变体 ID
@ApiProperty({
example: '1',
description: '对应WP产品表的ID',
type: 'number',
required: true,
})
@Column()
productId: number; // 对应WP产品表的 ID
@ApiProperty({ description: 'sku', type: 'string' })
@Column({ nullable: true })
sku?: string; // sku 编码
@ApiProperty({
description: '变体名称',
type: 'string',
required: true,
})
@Column()
name: string;
@ApiProperty({ description: '常规价格', type: Number })
@Column('decimal', { precision: 10, scale: 2, nullable: true })
regular_price: number; // 常规价格
@ApiProperty({ description: '销售价格', type: Number })
@Column('decimal', { precision: 10, scale: 2, nullable: true })
sale_price: number; // 销售价格
@ApiProperty({ description: '是否促销中', type: Boolean })
@Column({ nullable: true, type: Boolean })
on_sale: boolean; // 是否促销中
@ApiProperty({ description: '是否删除', type: Boolean })
@Column({ nullable: true, type: Boolean , default: false })
on_delete: boolean; // 是否删除
@Column({ type: 'json', nullable: true })
attributes: Record<string, any>; // 变体的属性
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
@ApiProperty({
description: '变体构成成分',
type: 'array',
items: {
type: 'object',
properties: {
sku: { type: 'string' },
quantity: { type: 'number' },
},
},
})
@Column('json', { nullable: true, comment: '变体构成成分' })
constitution: { sku: string; quantity: number }[] | null;
}

View File

@ -0,0 +1,125 @@
import {
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Unique,
Entity,
} from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger';
import { ProductStatus, ProductStockStatus, ProductType } from '../enums/base.enum';
@Entity('wp_product')
@Unique(['siteId', 'externalProductId']) // 确保产品的唯一性
export class WpProduct {
@ApiProperty({
example: '1',
description: 'ID',
type: 'number',
required: true,
})
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({
example: '1',
description: 'wp网站ID',
type: 'string',
required: true,
})
@Column()
siteId: string;
@ApiProperty({
example: '1',
description: 'wp产品ID',
type: 'string',
required: true,
})
@Column()
externalProductId: string;
@ApiProperty({ description: 'sku', type: 'string' })
@Column({ nullable: true })
sku?: string;
@ApiProperty({
example: 'ZYN 6MG WINTERGREEN',
description: '产品名称',
type: 'string',
required: true,
})
@Column()
name: string;
@ApiProperty({ description: '产品状态', enum: ProductStatus })
@Column({ type: 'enum', enum: ProductStatus })
status: ProductStatus;
@ApiProperty({ description: '上下架状态', enum: ProductStockStatus })
@Column({
name: 'stock_status',
type: 'enum',
enum: ProductStockStatus,
default: ProductStockStatus.INSTOCK
})
stockStatus: ProductStockStatus;
@ApiProperty({ description: '常规价格', type: Number })
@Column('decimal', { precision: 10, scale: 2, nullable: true })
regular_price: number; // 常规价格
@ApiProperty({ description: '销售价格', type: Number })
@Column('decimal', { precision: 10, scale: 2, nullable: true })
sale_price: number; // 销售价格
@ApiProperty({ description: '是否促销中', type: Boolean })
@Column({ nullable: true, type: Boolean })
on_sale: boolean; // 是否促销中
@ApiProperty({ description: '是否删除', type: Boolean })
@Column({ nullable: true, type: Boolean , default: false })
on_delete: boolean; // 是否删除
@ApiProperty({
description: '产品类型',
enum: ProductType,
})
@Column({ type: 'enum', enum: ProductType })
type: ProductType;
@Column({ type: 'json', nullable: true })
metadata: Record<string, any>; // 产品的其他扩展字段
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
@ApiProperty({
description: '产品构成成分',
type: 'array',
items: {
type: 'object',
properties: {
sku: { type: 'string' },
quantity: { type: 'number' },
},
},
})
@Column('json', { nullable: true, comment: '产品构成成分' })
constitution: { sku: string; quantity: number }[] | null;
}

87
src/enums/base.enum.ts Normal file
View File

@ -0,0 +1,87 @@
export enum ProductStatus {
PUBLISH = 'publish', // 已发布
DRAFT = 'draft', // 草稿
PENDING = 'pending', // 待审核
PRIVATE = 'private', // 私有
TRASH = 'trash', // 回收站
AUTO_DRAFT = 'auto-draft', // 自动草稿
FUTURE = 'future', // 定时发布
INHERIT = 'inherit', // 继承状态
}
export enum ProductStockStatus {
INSTOCK = 'instock',
OUT_OF_STOCK = 'outofstock',
ON_BACK_ORDER = 'onbackorder',
}
export enum ProductType {
SIMPLE = 'simple',
VARIABLE = 'variable',
WOOSB = 'woosb',
}
export enum PurchaseOrderStatus {
DRAFT = 'draft',
SUBMITTED = 'submitted',
RECEIVED = 'received',
}
export enum StockRecordOperationType {
IN = 'in',
OUT = 'out',
}
// Order status. Options: pending, processing, on-hold, completed, cancelled, refunded, failed and trash. Default is pending.
// 原始订单状态
export enum OrderStatus {
PENDING = 'pending', // default // 待付款
PROCESSING = 'processing', // 正在处理
ON_HOLD = 'on-hold', // 保留
COMPLETED = 'completed', // 已完成
CANCEL = 'cancelled', // 已取消
REFUNDED = 'refunded', // 已退款
FAILED = 'failed', // 失败订单
DRAFT = 'draft', // 草稿
AUTO_DRAFT = 'auto-draft', // 自动草稿
// TRASH = 'trash',
// refund 也就是退款相关的状态
RETURN_REQUESTED = 'return-requested', // 已申请退款
RETURN_APPROVED = 'return-approved', // 退款申请已通过
RETURN_CANCELLED = 'return-cancelled', // 已取消退款
}
export enum ErpOrderStatus {
PENDING = 'pending', // 待确认
PROCESSING = 'processing', //待发货
COMPLETED = 'completed', //已完成
CANCEL = 'cancelled', //已取消
REFUNDED = 'refunded', //已退款
FAILED = 'failed', //失败
AFTER_SALE_PROCESSING = 'after_sale_pending', // 售后处理中
PENDING_RESHIPMENT = 'pending_reshipment', // 待补发
PENDING_REFUND = 'pending_refund', // 待退款
RETURN_REQUESTED = 'return-requested', // 已申请退款
RETURN_APPROVED = 'return-approved', // 退款申请已通过
RETURN_CANCELLED = 'return-cancelled', // 已取消退款
}
export enum ShipmentType {
CANADAPOST = 'canadapost',
FREIGHTCOM = 'freightcom',
}
export enum staticValue {
// 万能验证码
STATIC_CAPTCHA = 'yoone2025!@YOONE0923'
}
// WooCommerce Subscription status
// Reference: https://woocommerce.com/document/subscriptions/statuses/
export enum SubscriptionStatus {
ACTIVE = 'active', // 活跃
PENDING = 'pending', // 待处理/待激活
ON_HOLD = 'on-hold', // 暂停
CANCELLED = 'cancelled', // 已取消
EXPIRED = 'expired', // 已过期
PENDING_CANCELLATION = 'pending-cancel', // 待取消
}

View File

@ -0,0 +1,13 @@
import { Catch } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
@Catch()
export class DefaultErrorFilter {
async catch(err: Error, ctx: Context) {
// 所有的未分类错误会到这里
return {
success: false,
message: err.message,
};
}
}

View File

@ -0,0 +1,10 @@
import { Catch, httpError, MidwayHttpError } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
@Catch(httpError.NotFoundError)
export class NotFoundFilter {
async catch(err: MidwayHttpError, ctx: Context) {
// 404 错误会到这里
ctx.redirect('/404.html');
}
}

View File

@ -1,33 +0,0 @@
import { useEffect, useState } from 'react';
import FingerprintJS from '@fingerprintjs/fingerprintjs';
/**
* Hook: 获取设备指纹(visitorId)
*/
export function useDeviceFingerprint() {
const [fingerprint, setFingerprint] = useState<string | null>(null);
useEffect(() => {
let isMounted = true;
async function loadFingerprint() {
try {
const fp = await FingerprintJS.load();
const result = await fp.get();
if (isMounted) {
setFingerprint(result.visitorId);
}
} catch (err) {
console.error('获取设备指纹失败:', err);
}
}
loadFingerprint();
return () => {
isMounted = false;
};
}, []);
return fingerprint; // 初始为 null加载后返回指纹 ID
}

23
src/interface.ts Normal file
View File

@ -0,0 +1,23 @@
/**
* @description User-Service parameters
*/
export interface IUserOptions {
uid: number;
}
export interface WpSite {
id: string;
wpApiUrl: string;
consumerKey: string;
consumerSecret: string;
siteName: string;
email: string;
emailPswd: string;
}
export interface PaginationParams {
current?: number; // 当前页码
pageSize?: number; // 每页数量
sorter?: any;
filter?: any;
}

View File

@ -0,0 +1,15 @@
import { FORMAT, ILogger, Logger } from '@midwayjs/core';
import { IJob, Job } from '@midwayjs/cron';
@Job({
cronTime: FORMAT.CRONTAB.EVERY_DAY,
runOnInit: true,
})
export class SyncProductJob implements IJob {
@Logger()
logger: ILogger;
onTick() {
}
onComplete?(result: any) {}
}

View File

@ -0,0 +1,87 @@
import { FORMAT, ILogger, Inject, Logger } from '@midwayjs/core';
import { IJob, Job } from '@midwayjs/cron';
import { LogisticsService } from '../service/logistics.service';
import { Repository } from 'typeorm';
import { Shipment } from '../entity/shipment.entity';
import { InjectEntityModel } from '@midwayjs/typeorm';
@Job({
cronTime: FORMAT.CRONTAB.EVERY_PER_30_MINUTE,
start: true,
runOnInit: true,
})
export class SyncShipmentJob implements IJob {
@Logger()
logger: ILogger;
@Inject()
logisticsService: LogisticsService;
onTick() {
this.logisticsService.syncShipmentStatus();
this.logisticsService.syncShipment();
}
onComplete?(result: any) {}
}
@Job({
cronTime: '0 0 12 * * *', // 每天12点执行
start: true
})
export class SyncUniuniShipmentJob implements IJob{
@Logger()
logger: ILogger;
@Inject()
logisticsService: LogisticsService;
@InjectEntityModel(Shipment)
shipmentModel: Repository<Shipment>
uniuniStateCodes = {
'190': 'ORDER_RECEIVED',
'192': 'CUSTOM_HOLD',
'195': 'GATEWAY_TRANSIT_OUT',
'198': 'CUSTOM_RELEASE_DIRECT',
'199': 'GATEWAY_TRANSIT',
'200': 'PARCEL_SCANNED',
'202': 'IN_TRANSIT',
'203': 'DELIVERED',
'204': 'TRANSSHIPMENT',
'206': 'WRONG_ADDRESS_FROM_TRANSIT',
'207': 'PARCEL_LOST',
'209': 'OTHER_EXCEPTION',
'211': 'RETURN_OFFICE_FROM_TRANSIT',
'212': 'WRONG_ADDRESS_FROM_RECEIVE',
'213': 'STORAGE_30_DAYS_FROM_OFFICE',
'214': 'STORAGE_30_DAYS_AFTER_SCAN',
'215': 'PARCEL_ABANDONED',
'216': 'SELF_PICK_UP',
'217': 'TRANSSHIPMENT_COMPLETE',
'218': 'SCANNED_PARCEL_MISSING',
'219': 'WRONG_ROUTE_PARCEL',
'220': 'SECOND_DELIVERY',
'221': 'RETURNED_PARCEL_SCANNED',
'222': 'REJECTED_PARCEL_FROM_TRANSIT',
'223': 'CHANGED_ORDER_RESENT',
'224': 'RESENT_ORDER_VOIDED',
'225': 'FORWARDED_3RDPARTY',
'226': 'STORAGE_3RDPARTY_SERVICE_POINT',
'228': 'SECOND_DELIVERED',
'229': 'DROP_OFF_SERVICE_POINTS',
'230': 'RETURN TO SENDER WAREHOUSE',
'231': 'FAILED_DELIVERY_RETRY1',
'232': 'FAILED_DELIVERY_RETRY2',
'255': 'Gateway_To_Gateway_Transit'
};
async onTick() {
const shipments:Shipment[] = await this.shipmentModel.findBy({ finished: false });
shipments.forEach(shipment => {
this.logisticsService.updateShipmentState(shipment);
});
}
onComplete(result: any) {
}
}

40
src/job/sync_tms.job.ts Normal file
View File

@ -0,0 +1,40 @@
import { ILogger, Inject, Logger } from '@midwayjs/core';
import { IJob, Job } from '@midwayjs/cron';
import { LogisticsService } from '../service/logistics.service';
import { Repository } from 'typeorm';
import { Shipment } from '../entity/shipment.entity';
import { InjectEntityModel } from '@midwayjs/typeorm';
@Job({
cronTime: '0 0 12 * * *', // 每天12点执行
start: true
})
export class SyncTmsJob implements IJob {
@Logger()
logger: ILogger;
@Inject()
logisticsService: LogisticsService;
@InjectEntityModel(Shipment)
shipmentModel: Repository<Shipment>
async onTick() {
const shipments: Shipment[] = await this.shipmentModel.findBy({ tracking_provider: 'freightwaves', finished: false });
const results = await Promise.all(
shipments.map(async shipment => {
return await this.logisticsService.updateFreightwavesShipmentState(shipment);
})
)
this.logger.info(`更新运单状态完毕 ${JSON.stringify(results)}`);
return results
}
onComplete(result: any) {
this.logger.info(`更新运单状态完成 ${result}`);
}
onError(error: any) {
this.logger.error(`更新运单状态失败 ${error.message}`);
}
}

View File

@ -0,0 +1,96 @@
// src/middleware/auth.middleware.ts
import {
IMiddleware,
Middleware,
Inject,
NextFunction,
httpError,
} from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
import { JwtService } from '@midwayjs/jwt'; // 引入 JwtService 类型
import { DeviceWhitelistService } from '../service/deviceWhitelist.service';
@Middleware()
export class AuthMiddleware implements IMiddleware<Context, NextFunction> {
@Inject()
jwtService: JwtService; // 注入 JwtService 实例
@Inject()
deviceWhitelistService: DeviceWhitelistService;
// 白名单配置
whiteList = [
'/user/login',
'/webhook/woocommerce',
'/logistics/getTrackingNumber',
'/logistics/getListByTrackingId',
];
match(ctx: Context) {
return !this.isWhiteListed(ctx);
}
resolve() {
return async (ctx: Context, next: NextFunction) => {
// 判断下有没有校验信息
if (!ctx.headers['authorization']) {
throw new httpError.UnauthorizedError();
}
// 从 header 上获取校验信息
const parts = ctx.get('authorization').trim().split(' ');
if (parts.length !== 2) {
throw new httpError.UnauthorizedError();
}
const [scheme, token] = parts;
if (!/^Bearer$/i.test(scheme)) {
throw new httpError.UnauthorizedError('Invalid Token Scheme');
}
try {
// 2. 校验 JWT 并解析 payload
const decoded: any = await this.jwtService.verify(token);
const deviceId: string = decoded.deviceId;
if (!deviceId) {
throw new httpError.UnauthorizedError('Missing deviceId in token');
}
// 3. 校验设备是否在白名单
const isWhite = await this.deviceWhitelistService.isWhitelisted(
deviceId
);
if (!isWhite) {
throw new httpError.UnauthorizedError('Device not authorized');
}
} catch (error) {
throw new httpError.UnauthorizedError('Invalid or expired token');
}
if (/^Bearer$/i.test(scheme)) {
try {
//jwt.verify方法验证token是否有效
await this.jwtService.verify(token, {
complete: true,
});
} catch (error) {
throw new httpError.UnauthorizedError();
}
await next();
}
};
}
static getName(): string {
return 'authMiddleware';
}
static getPriority(): number {
return 0;
}
isWhiteListed(ctx: Context): boolean {
return this.whiteList.includes(ctx.path);
}
}

View File

@ -0,0 +1,27 @@
import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';
@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {
resolve() {
return async (ctx: Context, next: NextFunction) => {
// 控制器前执行的逻辑
const startTime = Date.now();
// 执行下一个 Web 中间件,最后执行到控制器
// 这里可以拿到下一个中间件或者控制器的返回值
const result = await next();
// 控制器之后执行的逻辑
ctx.logger.info(
`Report in "src/middleware/report.middleware.ts", rt = ${
Date.now() - startTime
}ms`
);
// 返回给上一个中间件的结果
return result;
};
}
static getName(): string {
return 'report';
}
}

View File

@ -1,13 +0,0 @@
// 全局共享数据示例
import { DEFAULT_NAME } from '@/constants';
import { useState } from 'react';
const useUser = () => {
const [name, setName] = useState<string>(DEFAULT_NAME);
return {
name,
setName,
};
};
export default useUser;

View File

@ -1,286 +0,0 @@
import { HistoryOrder } from '@/pages/Statistics/Order';
import {
customercontrollerAddtag,
customercontrollerDeltag,
customercontrollerGetcustomerlist,
customercontrollerGettags,
customercontrollerSetrate,
} from '@/servers/api/customer';
import {
ActionType,
ModalForm,
PageContainer,
ProColumns,
ProFormSelect,
ProTable,
} from '@ant-design/pro-components';
import { App, Button, Rate, Space, Tag } from 'antd';
import dayjs from 'dayjs';
import { useRef, useState } from 'react';
const ListPage: React.FC = () => {
const actionRef = useRef<ActionType>();
const { message } = App.useApp();
const columns: ProColumns[] = [
{
title: '用户名',
dataIndex: 'username',
hideInSearch: true,
render: (_, record) => {
if (record.billing.first_name || record.billing.last_name)
return record.billing.first_name + ' ' + record.billing.last_name;
return record.shipping.first_name + ' ' + record.shipping.last_name;
},
},
{
title: '邮箱',
dataIndex: 'email',
},
{
title: '客户编号',
dataIndex: 'customerId',
render: (_, record) => {
if(!record.customerId) return '-';
return String(record.customerId).padStart(6,0)
},
sorter: true,
},
{
title: '首单时间',
dataIndex: 'first_purchase_date',
valueType: 'dateMonth',
sorter: true,
render: (_, record) =>
record.first_purchase_date
? dayjs(record.first_purchase_date).format('YYYY-MM-DD HH:mm:ss')
: '-',
// search: {
// transform: (value: string) => {
// return { month: value };
// },
// },
},
{
title: '尾单时间',
hideInSearch: true,
dataIndex: 'last_purchase_date',
valueType: 'dateTime',
sorter: true,
},
{
title: '订单数',
dataIndex: 'orders',
hideInSearch: true,
sorter: true,
},
{
title: '金额',
dataIndex: 'total',
hideInSearch: true,
sorter: true,
},
{
title: 'YOONE订单数',
dataIndex: 'yoone_orders',
hideInSearch: true,
sorter: true,
},
{
title: 'YOONE金额',
dataIndex: 'yoone_total',
hideInSearch: true,
sorter: true,
},
{
title: '等级',
hideInSearch: true,
render: (_, record) => {
if(!record.yoone_orders || !record.yoone_total) return '-'
if(Number(record.yoone_orders) === 1 && Number(record.yoone_total) > 0 ) return 'B'
return '-'
}
},
{
title: '评星',
dataIndex: 'rate',
width: 200,
render: (_, record) => {
return <Rate onChange={async(val)=>{
try{
const { success, message: msg } = await customercontrollerSetrate({
id: record.customerId,
rate: val
});
if (success) {
message.success(msg);
actionRef.current?.reload();
}
}catch(e){
message.error(e.message);
}
}} value={record.rate} />
},
},
{
title: 'phone',
dataIndex: 'phone',
hideInSearch: true,
render: (_, record) => record?.billing.phone || record?.shipping.phone,
},
{
title: 'state',
dataIndex: 'state',
render: (_, record) => record?.billing.state || record?.shipping.state,
},
{
title: 'city',
dataIndex: 'city',
hideInSearch: true,
render: (_, record) => record?.billing.city || record?.shipping.city,
},
{
title: '标签',
dataIndex: 'tags',
render: (_, record) => {
return (
<Space>
{(record.tags || []).map((tag) => {
return (
<Tag
key={tag}
closable
onClose={async () => {
const { success, message: msg } =
await customercontrollerDeltag({
email: record.email,
tag,
});
return false;
}}
>
{tag}
</Tag>
);
})}
</Space>
);
},
},
{
title: '操作',
dataIndex: 'option',
valueType: 'option',
render: (_, record) => {
return (
<Space>
<AddTag
email={record.email}
tags={record.tags}
tableRef={actionRef}
/>
<HistoryOrder
email={record.email}
tags={record.tags}
tableRef={actionRef}
/>
</Space>
);
},
},
];
return (
<PageContainer ghost>
<ProTable
headerTitle="查询表格"
actionRef={actionRef}
rowKey="id"
request={async (params, sorter) => {
const key = Object.keys(sorter)[0];
const { data, success } = await customercontrollerGetcustomerlist({
...params,
...(key ? { sorterKey: key, sorterValue: sorter[key] } : {}),
});
return {
total: data?.total || 0,
data: data?.items || [],
success,
};
}}
columns={columns}
/>
</PageContainer>
);
};
export const AddTag: React.FC<{
email: string;
tags?: string[];
tableRef: React.MutableRefObject<ActionType | undefined>;
}> = ({ email, tags, tableRef }) => {
const { message } = App.useApp();
const [tagList, setTagList] = useState<string[]>([]);
return (
<ModalForm
title={`修改标签 - ${email}`}
trigger={<Button></Button>}
width={800}
modalProps={{
destroyOnHidden: true,
}}
submitter={false}
>
<ProFormSelect
mode="tags"
allowClear
name="tag"
label="标签"
request={async () => {
const { data, success } = await customercontrollerGettags();
if (!success) return [];
setTagList(tags || []);
return data
.filter((tag) => {
return !(tags || []).includes(tag);
})
.map((tag) => ({ label: tag, value: tag }));
}}
fieldProps={{
value: tagList, // 当前值
onChange: async (newValue) => {
const added = newValue.filter((x) => !tagList.includes(x));
const removed = tagList.filter((x) => !newValue.includes(x));
if (added.length) {
const { success, message: msg } = await customercontrollerAddtag({
email,
tag: added[0],
});
if (!success) {
message.error(msg);
return;
}
}
if (removed.length) {
const { success, message: msg } = await customercontrollerDeltag({
email,
tag: removed[0],
});
if (!success) {
message.error(msg);
return;
}
}
tableRef?.current?.reload();
setTagList(newValue);
},
}}
></ProFormSelect>
</ModalForm>
);
};
export default ListPage;

View File

@ -1,3 +0,0 @@
.container {
padding-top: 80px;
}

Some files were not shown because too many files have changed in this diff Show More