feat: 初始化项目

This commit is contained in:
R524809
2026-09-02 17:47:25 +08:00
parent 04c94e75db
commit 1a2caba2b3
51 changed files with 6793 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
POSTGRES_USER=atlas_app_login
POSTGRES_PASSWORD=change-me-local-only
POSTGRES_DB=atlas
POSTGRES_PORT=5432
REDIS_URL=redis://localhost:6379
MINIO_ROOT_USER=atlas_minio_local
MINIO_ROOT_PASSWORD=change-me-local-only
MINIO_PORT=9000
MINIO_CONSOLE_PORT=9001
API_PORT=8200
WEB_PORT=8100
+11
View File
@@ -15,3 +15,14 @@ dist/
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*.log *.log
node_modules/
.pnpm-store/
.turbo/
dist/
coverage/
.env
.env.*
!.env.example
*.log
.DS_Store
data/
+1
View File
@@ -0,0 +1 @@
24.20.0
+7
View File
@@ -0,0 +1,7 @@
node_modules/
.turbo/
dist/
apps/extension/.output/
apps/extension/.wxt/
docs/**
!docs/architecture/project-structure.md
+25
View File
@@ -0,0 +1,25 @@
{
"name": "@atlas/api",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
"dev": "tsx watch src/main.ts",
"start": "node dist/main.js",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@atlas/contracts": "workspace:*",
"@nestjs/common": "12.0.1",
"@nestjs/core": "12.0.1",
"@nestjs/platform-express": "12.0.1",
"reflect-metadata": "0.2.2",
"rxjs": "7.8.2"
},
"devDependencies": {
"@types/node": "26.3.0",
"tsx": "4.23.13",
"typescript": "7.0.2"
}
}
+12
View File
@@ -0,0 +1,12 @@
import { Controller, Get, Module } from '@nestjs/common';
@Controller('health')
class HealthController {
@Get()
check() {
return { status: 'ok', service: 'api' };
}
}
@Module({ controllers: [HealthController] })
export class AppModule {}
+12
View File
@@ -0,0 +1,12 @@
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module.js';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors({ origin: process.env.WEB_ORIGIN ?? 'http://localhost:8100' });
await app.listen(Number(process.env.API_PORT ?? 8200));
}
void bootstrap();
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"types": ["node"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*.ts"]
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,36 @@
{
"manifest_version": 3,
"name": "Atlas Seller Kit Collector",
"description": "将已确认的来源商品采集到 Atlas 工作空间。",
"version": "0.1.0",
"permissions": [
"storage",
"identity",
"tabs",
"scripting"
],
"host_permissions": [
"*://*.ozon.ru/*",
"*://*.ozon.kz/*",
"*://*.ozon.by/*",
"*://*.1688.com/*",
"*://*.taobao.com/*",
"*://*.tmall.com/*",
"http://localhost/*"
],
"commands": {
"wxt:reload-extension": {
"description": "Reload the extension during development",
"suggested_key": {
"default": "Alt+R"
}
}
},
"background": {
"service_worker": "background.js"
},
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval' http://localhost:3000; object-src 'self';",
"sandbox": "script-src 'self' 'unsafe-inline' 'unsafe-eval' http://localhost:3000; sandbox allow-scripts allow-forms allow-popups allow-modals; child-src 'self';"
}
}
@@ -0,0 +1 @@
var background=(function(){function e(e){return e==null||typeof e==`function`?{main:e}:e}var t=e(()=>{console.info(`Atlas collector background started.`)});globalThis.browser?.runtime?.id?globalThis.browser:globalThis.chrome;var n={debug:(...e)=>([...e],void 0),log:(...e)=>([...e],void 0),warn:(...e)=>([...e],void 0),error:(...e)=>([...e],void 0)},r;try{r=t.main(),r instanceof Promise&&console.warn(`The background's main() function return a promise, but it must be synchronous`)}catch(e){throw n.error(`The background crashed on startup!`),e}return r})();
@@ -0,0 +1 @@
{"manifest_version":3,"name":"Atlas Seller Kit Collector","description":"将已确认的来源商品采集到 Atlas 工作空间。","version":"0.1.0","permissions":["storage","identity"],"host_permissions":["*://*.ozon.ru/*","*://*.ozon.kz/*","*://*.ozon.by/*","*://*.1688.com/*","*://*.taobao.com/*","*://*.tmall.com/*"],"background":{"service_worker":"background.js"}}
+55
View File
@@ -0,0 +1,55 @@
{
"compilerOptions": {
"lib": [
"ESNext",
"DOM",
"DOM.Iterable"
],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"paths": {
"@": [
".."
],
"@/*": [
"../*"
],
"~": [
".."
],
"~/*": [
"../*"
],
"@@": [
".."
],
"@@/*": [
"../*"
],
"~~": [
".."
],
"~~/*": [
"../*"
]
}
},
"include": [
"../**/*",
"./wxt.d.ts"
],
"exclude": [
"../**/node_modules",
"../.output"
]
}
+15
View File
@@ -0,0 +1,15 @@
// Generated by wxt
interface ImportMetaEnv {
readonly MANIFEST_VERSION: 2 | 3;
readonly BROWSER: string;
readonly CHROME: boolean;
readonly FIREFOX: boolean;
readonly SAFARI: boolean;
readonly EDGE: boolean;
readonly OPERA: boolean;
readonly COMMAND: "build" | "serve";
readonly ENTRYPOINT: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
+81
View File
@@ -0,0 +1,81 @@
// Generated by wxt
import "wxt/browser";
declare module "wxt/browser" {
/**
* See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
*/
interface GetMessageOptions {
/**
* See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
*/
escapeLt?: boolean
}
export interface WxtI18n extends I18n.Static {
/**
* The extension or app ID; you might use this string to construct URLs for resources inside the extension. Even non-localized extensions can use this message.
* Note: You can't use this message in a manifest file.
*
* "<browser.runtime.id>"
*/
getMessage(
messageName: "@@extension_id",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
/**
* "<browser.i18n.getUiLocale()>"
*/
getMessage(
messageName: "@@ui_locale",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
/**
* The text direction for the current locale, either "ltr" for left-to-right languages such as English or "rtl" for right-to-left languages such as Japanese.
*
* "<ltr|rtl>"
*/
getMessage(
messageName: "@@bidi_dir",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
/**
* If the @@bidi_dir is "ltr", then this is "rtl"; otherwise, it's "ltr".
*
* "<rtl|ltr>"
*/
getMessage(
messageName: "@@bidi_reversed_dir",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
/**
* If the @@bidi_dir is "ltr", then this is "left"; otherwise, it's "right".
*
* "<left|right>"
*/
getMessage(
messageName: "@@bidi_start_edge",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
/**
* If the @@bidi_dir is "ltr", then this is "right"; otherwise, it's "left".
*
* "<right|left>"
*/
getMessage(
messageName: "@@bidi_end_edge",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
getMessage(
messageName: "@@extension_id" | "@@ui_locale" | "@@bidi_dir" | "@@bidi_reversed_dir" | "@@bidi_start_edge" | "@@bidi_end_edge",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
}
}
+21
View File
@@ -0,0 +1,21 @@
// Generated by wxt
// Types for the #import virtual module
declare module '#imports' {
export { browser, Browser } from 'wxt/browser';
export { storage, WxtStorage, WxtStorageItem, StorageArea, StorageItemKey, StorageAreaChanges, MigrationError } from 'wxt/utils/storage';
export { getAppConfig, useAppConfig } from 'wxt/utils/app-config';
export { ContentScriptContext, WxtWindowEventMap } from 'wxt/utils/content-script-context';
export { createIframeUi, IframeContentScriptUi, IframeContentScriptUiOptions } from 'wxt/utils/content-script-ui/iframe';
export { createIntegratedUi, IntegratedContentScriptUi, IntegratedContentScriptUiOptions } from 'wxt/utils/content-script-ui/integrated';
export { createShadowRootUi, ShadowRootContentScriptUi, ShadowRootContentScriptUiOptions } from 'wxt/utils/content-script-ui/shadow-root';
export { ContentScriptUi, ContentScriptUiOptions, ContentScriptOverlayAlignment, ContentScriptAppendMode, ContentScriptInlinePositioningOptions, ContentScriptOverlayPositioningOptions, ContentScriptModalPositioningOptions, ContentScriptPositioningOptions, ContentScriptAnchoredOptions, AutoMountOptions, StopAutoMount, AutoMount } from 'wxt/utils/content-script-ui/types';
export { defineAppConfig, WxtAppConfig } from 'wxt/utils/define-app-config';
export { defineBackground } from 'wxt/utils/define-background';
export { defineContentScript } from 'wxt/utils/define-content-script';
export { defineUnlistedScript } from 'wxt/utils/define-unlisted-script';
export { defineWxtPlugin } from 'wxt/utils/define-wxt-plugin';
export { injectScript, ScriptPublicPath, InjectScriptOptions } from 'wxt/utils/inject-script';
export { InvalidMatchPattern, MatchPattern } from 'wxt/utils/match-patterns';
export { useState, useCallback, useMemo, useEffect, useRef, useContext, useReducer } from 'react';
export { fakeBrowser } from 'wxt/testing/fake-browser';
}
+58
View File
@@ -0,0 +1,58 @@
// Generated by wxt
export {}
declare global {
const ContentScriptContext: typeof import('wxt/utils/content-script-context').ContentScriptContext
const InvalidMatchPattern: typeof import('wxt/utils/match-patterns').InvalidMatchPattern
const MatchPattern: typeof import('wxt/utils/match-patterns').MatchPattern
const browser: typeof import('wxt/browser').browser
const createIframeUi: typeof import('wxt/utils/content-script-ui/iframe').createIframeUi
const createIntegratedUi: typeof import('wxt/utils/content-script-ui/integrated').createIntegratedUi
const createShadowRootUi: typeof import('wxt/utils/content-script-ui/shadow-root').createShadowRootUi
const defineAppConfig: typeof import('wxt/utils/define-app-config').defineAppConfig
const defineBackground: typeof import('wxt/utils/define-background').defineBackground
const defineContentScript: typeof import('wxt/utils/define-content-script').defineContentScript
const defineUnlistedScript: typeof import('wxt/utils/define-unlisted-script').defineUnlistedScript
const defineWxtPlugin: typeof import('wxt/utils/define-wxt-plugin').defineWxtPlugin
const fakeBrowser: typeof import('wxt/testing/fake-browser').fakeBrowser
const getAppConfig: typeof import('wxt/utils/app-config').getAppConfig
const injectScript: typeof import('wxt/utils/inject-script').injectScript
const storage: typeof import('wxt/utils/storage').storage
const useAppConfig: typeof import('wxt/utils/app-config').useAppConfig
const useCallback: typeof import('react').useCallback
const useContext: typeof import('react').useContext
const useEffect: typeof import('react').useEffect
const useMemo: typeof import('react').useMemo
const useReducer: typeof import('react').useReducer
const useRef: typeof import('react').useRef
const useState: typeof import('react').useState
}
// for type re-export
declare global {
// @ts-ignore
export type { Browser } from 'wxt/browser'
import('wxt/browser')
// @ts-ignore
export type { WxtStorage, WxtStorageItem, StorageArea, StorageItemKey, StorageAreaChanges, MigrationError } from 'wxt/utils/storage'
import('wxt/utils/storage')
// @ts-ignore
export type { WxtWindowEventMap } from 'wxt/utils/content-script-context'
import('wxt/utils/content-script-context')
// @ts-ignore
export type { IframeContentScriptUi, IframeContentScriptUiOptions } from 'wxt/utils/content-script-ui/iframe'
import('wxt/utils/content-script-ui/iframe')
// @ts-ignore
export type { IntegratedContentScriptUi, IntegratedContentScriptUiOptions } from 'wxt/utils/content-script-ui/integrated'
import('wxt/utils/content-script-ui/integrated')
// @ts-ignore
export type { ShadowRootContentScriptUi, ShadowRootContentScriptUiOptions } from 'wxt/utils/content-script-ui/shadow-root'
import('wxt/utils/content-script-ui/shadow-root')
// @ts-ignore
export type { ContentScriptUi, ContentScriptUiOptions, ContentScriptOverlayAlignment, ContentScriptAppendMode, ContentScriptInlinePositioningOptions, ContentScriptOverlayPositioningOptions, ContentScriptModalPositioningOptions, ContentScriptPositioningOptions, ContentScriptAnchoredOptions, AutoMountOptions, StopAutoMount, AutoMount } from 'wxt/utils/content-script-ui/types'
import('wxt/utils/content-script-ui/types')
// @ts-ignore
export type { WxtAppConfig } from 'wxt/utils/define-app-config'
import('wxt/utils/define-app-config')
// @ts-ignore
export type { ScriptPublicPath, InjectScriptOptions } from 'wxt/utils/inject-script'
import('wxt/utils/inject-script')
}
+14
View File
@@ -0,0 +1,14 @@
// Generated by wxt
import "wxt/browser";
declare module "wxt/browser" {
export type PublicPath =
| ""
| "/"
| "/background.js"
type HtmlPublicPath = Extract<PublicPath, `${string}.html`>
export interface WxtRuntime {
getURL(path: PublicPath): string;
getURL(path: `${HtmlPublicPath}${string}`): string;
}
}
+8
View File
@@ -0,0 +1,8 @@
// Generated by wxt
/// <reference types="wxt/vite-builder-env" />
/// <reference types="@wxt-dev/module-react" />
/// <reference path="./types/paths.d.ts" />
/// <reference path="./types/i18n.d.ts" />
/// <reference path="./types/globals.d.ts" />
/// <reference path="./types/imports-module.d.ts" />
/// <reference path="./types/imports.d.ts" />
+3
View File
@@ -0,0 +1,3 @@
export default defineBackground(() => {
console.info('Atlas collector background started.');
});
+23
View File
@@ -0,0 +1,23 @@
{
"name": "@atlas/extension",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "wxt build",
"dev": "wxt",
"typecheck": "wxt prepare && tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@atlas/contracts": "workspace:*",
"@wxt-dev/module-react": "1.2.2",
"react": "19.2.8",
"react-dom": "19.2.8"
},
"devDependencies": {
"@types/react": "19.2.18",
"@types/react-dom": "19.2.5",
"typescript": "7.0.2",
"wxt": "0.21.4"
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"noEmit": true
},
"include": ["**/*.ts", "**/*.tsx", ".wxt/**/*.ts"]
}
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig } from 'wxt';
export default defineConfig({
modules: ['@wxt-dev/module-react'],
manifest: {
name: 'Atlas Seller Kit Collector',
description: '将已确认的来源商品采集到 Atlas 工作空间。',
permissions: ['storage', 'identity'],
host_permissions: [
'*://*.ozon.ru/*',
'*://*.ozon.kz/*',
'*://*.ozon.by/*',
'*://*.1688.com/*',
'*://*.taobao.com/*',
'*://*.tmall.com/*',
],
},
});
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Atlas Seller Kit</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+25
View File
@@ -0,0 +1,25 @@
{
"name": "@atlas/web",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -b && vite build",
"dev": "vite",
"typecheck": "tsc -b --pretty false"
},
"dependencies": {
"antd": "6.6.2",
"axios": "1.20.0",
"react": "19.2.8",
"react-dom": "19.2.8",
"react-router-dom": "7.18.3"
},
"devDependencies": {
"@types/react": "19.2.18",
"@types/react-dom": "19.2.5",
"@vitejs/plugin-react": "6.1.1",
"typescript": "7.0.2",
"vite": "8.2.2"
}
}
+21
View File
@@ -0,0 +1,21 @@
import { App as AntdApp, ConfigProvider, Typography } from 'antd';
import { createRoot } from 'react-dom/client';
import 'antd/dist/reset.css';
function Root() {
return (
<ConfigProvider>
<AntdApp>
<main style={{ margin: '64px auto', maxWidth: 840, padding: '0 24px' }}>
<Typography.Title>Atlas Seller Kit</Typography.Title>
<Typography.Paragraph>
卖家商品工作台初始化完成。账户、平台上下文和门店模块将在此基础上实现。
</Typography.Paragraph>
</main>
</AntdApp>
</ConfigProvider>
);
}
createRoot(document.getElementById('root')!).render(<Root />);
+3
View File
@@ -0,0 +1,3 @@
/// <reference types="vite/client" />
declare module '*.css';
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"noEmit": true,
"allowImportingTsExtensions": false
},
"include": ["src", "vite.config.ts"]
}
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: Number(process.env.WEB_PORT ?? 8100),
proxy: {
'/api': {
target: process.env.API_ORIGIN ?? 'http://localhost:8200',
changeOrigin: true,
},
},
},
});
+20
View File
@@ -0,0 +1,20 @@
{
"name": "@atlas/worker",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "tsc -p tsconfig.json",
"dev": "tsx watch src/main.ts",
"start": "node dist/main.js",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@atlas/contracts": "workspace:*",
"bullmq": "6.3.4"
},
"devDependencies": {
"@types/node": "26.3.0",
"tsx": "4.23.13",
"typescript": "7.0.2"
}
}
+20
View File
@@ -0,0 +1,20 @@
import { Queue } from 'bullmq';
const connection = { url: process.env.REDIS_URL ?? 'redis://localhost:6379' };
const dispatchQueue = new Queue('atlas-dispatch', { connection });
async function bootstrap() {
await dispatchQueue.waitUntilReady();
console.info(
'Atlas Worker is ready; it will consume transactional-outbox events after that module exists.',
);
}
void bootstrap();
async function shutdown() {
await dispatchQueue.close();
}
process.once('SIGINT', () => void shutdown());
process.once('SIGTERM', () => void shutdown());
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"types": ["node"]
},
"include": ["src/**/*.ts"]
}
+1
View File
@@ -10,6 +10,7 @@
| 产品需求 | [`requirements/`](./requirements/) | 页面、流程、字段和待确认事项 | | 产品需求 | [`requirements/`](./requirements/) | 页面、流程、字段和待确认事项 |
| 基础架构 | [`architecture/`](./architecture/) | 多租户、平台、门店、水印、物流和定价的数据设计及 DDL | | 基础架构 | [`architecture/`](./architecture/) | 多租户、平台、门店、水印、物流和定价的数据设计及 DDL |
| 商品领域设计 | [`product/`](./product/) | 已有的多 SKU 商品编辑页、领域模型与 API 草案 | | 商品领域设计 | [`product/`](./product/) | 已有的多 SKU 商品编辑页、领域模型与 API 草案 |
| 项目结构 | [`architecture/project-structure.md`](./architecture/project-structure.md) | pnpm monorepo 的目录、依赖关系与本地运行边界。 |
| 外部平台资料 | [`integrations/`](./integrations/) | Ozon Seller API 的整理资料与适配依据 | | 外部平台资料 | [`integrations/`](./integrations/) | Ozon Seller API 的整理资料与适配依据 |
| 学习指南 | [`guides/`](./guides/) | 项目所用技术概念与设计原则的入门说明 | | 学习指南 | [`guides/`](./guides/) | 项目所用技术概念与设计原则的入门说明 |
| HTTP API 设计 | [`api/`](./api/) | 可评审的服务端接口契约,按模块逐步补全。 | | HTTP API 设计 | [`api/`](./api/) | 可评审的服务端接口契约,按模块逐步补全。 |
+126
View File
@@ -0,0 +1,126 @@
# 项目结构与运行边界
> 状态:初始化骨架已创建。本文描述仓库的代码归属、构建关系和本地运行边界;业务规则仍以项目交接、API 契约和数据模型文档为准。
## 1. 目录结构
```text
atlas-seller-kit/
├── apps/ # 可独立启动、构建和部署的入口
│ ├── web/ # React + Vite 卖家工作台
│ ├── api/ # NestJS:HTTP、鉴权、事务与任务创建
│ ├── worker/ # BullMQ:Outbox 投递、媒体、AI、同步、发布
│ └── extension/ # WXT 浏览器采集扩展
├── packages/ # 跨应用的稳定复用单元
│ ├── contracts/ # Zod 契约、DTO、共享类型
│ └── database/ # SQL migrations、迁移/应用连接基础设施
├── infra/
│ ├── compose.yaml # local PostgreSQL、Redis、MinIO
│ └── README.md # 本地基础设施启动说明
├── docs/ # 产品、架构、API 与评审记录
├── package.json # 根命令、Node/pnpm 版本约束
├── .nvmrc # 本地 Node 24 LTS 版本
├── pnpm-workspace.yaml # workspace 边界
├── turbo.json # 构建、检查、测试任务图
└── tsconfig.base.json # 最小共享 TypeScript 编译约定
```
`apps` 只能依赖明确的 `packages`,不能横向导入另一个应用的源码。这样 API、Worker 和扩展即使独立部署,也始终共享同一份契约。反过来,`packages` 不得依赖任何 `apps`。
## 2. 代码与服务关系图
```mermaid
flowchart TB
subgraph Repository[Atlas Seller Kit · pnpm workspace]
subgraph Applications[apps:部署与运行入口]
WEB[web\nReact / Vite]
API[api\nNestJS HTTP API]
WORKER[worker\nBullMQ processors]
EXT[extension\nWXT Collector]
end
subgraph Shared[packages:跨应用稳定边界]
CONTRACTS[contracts\nZod · DTO · payload types]
DB[database\nSQL migrations · DB access]
end
subgraph LocalInfra[infra/compose.yaml:本地依赖]
PG[(PostgreSQL)]
REDIS[(Redis)]
MINIO[(MinIO)]
end
end
WEB -->|HTTPS /api| API
EXT -->|Bearer API request| API
API --> CONTRACTS
EXT --> CONTRACTS
API --> DB
WORKER --> DB
API -->|transactional outbox| PG
API --> REDIS
WORKER -->|consume / retry| REDIS
WORKER --> PG
WORKER --> MINIO
```
## 3. 运行时责任与数据流
```mermaid
sequenceDiagram
participant U as 用户
participant E as 浏览器扩展
participant W as Web
participant A as API
participant P as PostgreSQL
participant R as Redis / BullMQ
participant K as Worker
participant O as MinIO / Kodo
U->>E: 在来源商品页采集
E->>A: 共享契约校验后的采集载荷
A->>P: 同一事务写业务记录、operation、outbox event
A-->>E: 采集记录 / 编辑入口
K->>P: Dispatcher 领取未投递 outbox event
K->>R: 投递或消费可重试任务
K->>O: 下载、处理、转存素材
K->>P: 保存任务状态、结果与错误
U->>W: 查看进度、编辑、发布
W->>A: 鉴权后的 HTTP 请求
A->>P: SET LOCAL app.workspace_id 后读写
```
业务事实、审计、操作状态和 Outbox 只在 PostgreSQL 持久化;Redis/BullMQ 负责排队、限流和重试,不能成为唯一事实来源。local 使用 MinIO,staging/production 使用隔离的七牛 Kodo Bucket。
## 4. 依赖规则
| 位置 | 可以依赖 | 不应依赖 |
|---|---|---|
| `apps/web` | `packages/contracts`、前端库 | API/Worker 源码、数据库直连 |
| `apps/api` | `packages/contracts`、`packages/database` | Web/扩展源码 |
| `apps/worker` | `packages/contracts`、`packages/database` | Web/扩展源码、浏览器会话 |
| `apps/extension` | `packages/contracts` | API 内部模块、ERP localStorage token |
| `packages/contracts` | Zod 等无运行环境依赖库 | NestJS、React、数据库驱动 |
| `packages/database` | PostgreSQL 驱动、迁移工具 | HTTP/UI/平台适配逻辑 |
当前只抽取了已经确认会跨端复用的采集契约和数据库边界。认证、商品、定价或 Ozon 适配器尚不单独建共享包,等首个纵向闭环出现稳定复用关系后再提取,避免过早形成空壳“公共库”。
## 5. 本地开发入口
```bash
cp .env.example .env
pnpm install
pnpm infra:up # PostgreSQL、Redis、MinIO
pnpm dev # Web、API、Worker、Extension 并行 watch
```
常用验证命令:`pnpm build`、`pnpm typecheck`、`pnpm format:check`。首次实现数据库迁移执行器时,必须将 `atlas_migrator` 和 `atlas_app_login` 的连接串分开配置,并在集成测试中验证 RLS 隔离。
## 6. 接下来的实现顺序
1. 在 `packages/database` 实现只接受 migrator 连接串的可重复 SQL migration runner,并导入已评审的 `001_foundation.sql`、`002_product-domain.sql`。
2. 在 API 建立配置校验、认证、工作空间上下文和事务包装器;任何工作空间查询前固定 `SET LOCAL app.workspace_id`。
3. 建立 `operations`、幂等请求和 Outbox Dispatcher 的第一个集成测试闭环。
4. 再实现平台/门店、素材上传和 Web 对应页面;扩展仅在 API 采集入口稳定后接入实际解析器。
相关约束见:[技术与部署方案](./technology-and-deployment.md)、[数据库安全](./database-security.md)、[Outbox 与幂等](./transactional-outbox-and-idempotency.md)、[项目交接](../project/HANDOFF.md)。
@@ -16,6 +16,8 @@
不将旧项目的 FastAPI 服务作为新项目运行时依赖;旧项目的 Ozon 资料、计价规则、采集经验和前端技术路线可作为参考或选择性复用。 不将旧项目的 FastAPI 服务作为新项目运行时依赖;旧项目的 Ozon 资料、计价规则、采集经验和前端技术路线可作为参考或选择性复用。
代码仓库采用 pnpm workspace monorepo:Web、API、Worker 与浏览器扩展位于独立 `apps/*`,跨端 API 契约和数据库迁移基础设施位于 `packages/*`。目录和依赖边界见 [项目结构与运行边界](./project-structure.md)。
## 2. 服务边界 ## 2. 服务边界
```text ```text
+5
View File
@@ -0,0 +1,5 @@
# 本地基础设施
复制根目录 `.env.example` 为 `.env` 后,执行 `pnpm infra:up` 启动 PostgreSQL、Redis 和 MinIO。镜像使用已验证的明确版本标签;升级前应先在本地或 staging 验证,再显式更新标签。
应用进程由 `pnpm dev` 在宿主机启动:Web 使用 Vite 热更新,API 与 Worker 使用各自的 watch 模式。生产环境的 API、Worker 和反向代理容器编排将在部署运行手册中补充;PostgreSQL 和对象存储继续遵守现有架构文档的托管服务边界。
+44
View File
@@ -0,0 +1,44 @@
name: atlas-seller-kit
services:
postgres:
image: postgres:18.6-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER:-atlas_app_login}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-change-me-local-only}
POSTGRES_DB: ${POSTGRES_DB:-atlas}
ports:
- '${POSTGRES_PORT:-5432}:5432'
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB']
interval: 5s
timeout: 5s
retries: 20
redis:
image: redis:7.4.11-alpine3.21
ports:
- '6379:6379'
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 5s
timeout: 5s
retries: 20
minio:
image: minio/minio:RELEASE.2025-09-07T16-13-09Z
command: server /data --console-address ':9001'
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-atlas_minio_local}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-change-me-local-only}
ports:
- '${MINIO_PORT:-9000}:9000'
- '${MINIO_CONSOLE_PORT:-9001}:9001'
volumes:
- minio-data:/data
volumes:
postgres-data:
minio-data:
+26
View File
@@ -0,0 +1,26 @@
{
"name": "atlas-seller-kit",
"version": "0.1.0",
"private": true,
"packageManager": "pnpm@11.25.0",
"engines": {
"node": ">=24.20.0 <25",
"pnpm": ">=11.25.0 <12"
},
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --parallel",
"lint": "turbo run lint",
"typecheck": "turbo run typecheck",
"test": "turbo run test",
"format": "prettier --write .",
"format:check": "prettier --check .",
"infra:up": "docker compose -f infra/compose.yaml up -d postgres redis minio",
"infra:down": "docker compose -f infra/compose.yaml down"
},
"devDependencies": {
"prettier": "3.9.6",
"turbo": "2.10.12",
"typescript": "7.0.2"
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "@atlas/contracts",
"version": "0.1.0",
"private": true,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"zod": "4.5.4"
},
"devDependencies": {
"typescript": "7.0.2"
}
}
+24
View File
@@ -0,0 +1,24 @@
import { z } from 'zod';
/**
* 浏览器扩展与 API 共享的采集载荷最小契约。
* 领域字段会随采集模块实现逐步补全,不能在扩展侧绕过此契约直接构造 API 请求。
*/
export const collectedProductPayloadSchema = z.object({
source: z.object({
platform: z.enum(['ozon', '1688', 'taobao', 'tmall']),
itemKey: z.string().min(1),
canonicalUrl: z.url(),
collectedAt: z.iso.datetime(),
parserVersion: z.string().min(1),
}),
common: z.object({
title: z.string().min(1).optional(),
}),
variantAxes: z.array(z.unknown()),
variants: z.array(z.unknown()),
assets: z.array(z.unknown()),
evidence: z.array(z.unknown()),
});
export type CollectedProductPayload = z.infer<typeof collectedProductPayloadSchema>;
+1
View File
@@ -0,0 +1 @@
export * from './collection';
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}
+5
View File
@@ -0,0 +1,5 @@
# SQL migrations
此目录只保存按序号命名、不可修改历史的 SQL migration,例如 `001_foundation.sql`。
迁移执行器必须使用 `atlas_migrator` 连接;API 和 Worker 只允许使用 `atlas_app_login`。每个涉及工作空间数据的应用事务,都必须在查询前执行 `SET LOCAL app.workspace_id`。详细权限规则见 [`docs/architecture/database-security.md`](../../../docs/architecture/database-security.md)。
+16
View File
@@ -0,0 +1,16 @@
{
"name": "@atlas/database",
"version": "0.1.0",
"private": true,
"scripts": {
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"pg": "8.23.0"
},
"devDependencies": {
"@types/node": "26.3.0",
"@types/pg": "8.23.1",
"typescript": "7.0.2"
}
}
+5
View File
@@ -0,0 +1,5 @@
/**
* 数据库访问层的唯一入口。
* 后续在此封装迁移连接与应用连接,应用事务必须先设置 app.workspace_id。
*/
export const databasePackageName = '@atlas/database';
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}
+5602
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
packages:
- apps/*
- packages/*
allowBuilds:
esbuild: true
msgpackr-extract: false
+5
View File
@@ -0,0 +1,5 @@
export default {
singleQuote: true,
trailingComma: 'all',
printWidth: 100,
};
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2023",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}
+24
View File
@@ -0,0 +1,24 @@
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".output/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^lint"]
},
"typecheck": {
"dependsOn": ["^typecheck"],
"outputs": []
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
}
}
}