@posx/core 5.5.392 → 5.5.395
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +23 -23
- package/LICENSE +21 -21
- package/README.md +85 -85
- package/build/index.d.ts +6 -0
- package/build/index.js +1 -1
- package/jest.config.cjs +36 -36
- package/jest.setup.cjs +80 -80
- package/memo/technical-docs/01_ARCHITECTURE.md +147 -0
- package/memo/technical-docs/02_CORE_BUSINESS.md +292 -0
- package/memo/technical-docs/03_UI_COMPONENTS.md +59 -0
- package/memo/technical-docs/04_VIEWS.md +82 -0
- package/memo/technical-docs/05_DATA_LAYER.md +375 -0
- package/memo/technical-docs/06_CROSS_PLATFORM.md +246 -0
- package/memo/technical-docs/07_SIMILARITY_INDEX.md +195 -0
- package/memo/technical-docs/CHECKPOINT.md +46 -0
- package/memo/technical-docs/PROJECT_OVERVIEW.md +122 -0
- package/memo/technical-docs/TECHNICAL_DOCS_PLAN.md +77 -0
- package/package.json +1 -1
- package/package.publish.json +121 -120
- package/tsdown.config.ts +21 -21
- package/vite.config.ts +86 -86
- package/dev/.stfolder/syncthing-folder-9a95b7.txt +0 -5
- package/dev/98894488.xlsx +0 -0
- package/dev/HappyThaiSembawang.csv +0 -336
- package/dev/KB/create-new-model.md +0 -34
- package/dev/KB/markdown-lint.md +0 -14
- package/dev/KB/readmefirst.md +0 -6
- package/dev/Merchants/HappyThaiSembawang.csv +0 -400
- package/dev/Merchants/HappyThaiSembawang.xlsx +0 -0
- package/dev/Product_Import_Template.xlsx +0 -0
- package/dev/XPOS Invoice Module.pdf +0 -232
- package/dev/harbor-harness-deployment.md +0 -78
- package/dev/nginx-harbor-harness.conf +0 -84
- package/dev/test-logs/2026-02.md +0 -5
- package/dev//344/272/247/345/223/201/345/257/274/345/205/245/346/250/241/346/235/277.xlsx +0 -0
package/jest.config.cjs
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
// jest.config.js
|
|
2
|
-
module.exports = {
|
|
3
|
-
preset: 'ts-jest',
|
|
4
|
-
testEnvironment: 'jsdom',
|
|
5
|
-
setupFilesAfterEnv: ['<rootDir>/jest.setup.cjs'],
|
|
6
|
-
transform: {
|
|
7
|
-
'^.+\\.ts$': ['ts-jest', {
|
|
8
|
-
tsconfig: {
|
|
9
|
-
module: 'commonjs',
|
|
10
|
-
esModuleInterop: true,
|
|
11
|
-
}
|
|
12
|
-
}],
|
|
13
|
-
'^.+\\.(mjs|js)$': ['ts-jest', {
|
|
14
|
-
tsconfig: {
|
|
15
|
-
allowJs: true,
|
|
16
|
-
module: 'commonjs',
|
|
17
|
-
esModuleInterop: true,
|
|
18
|
-
}
|
|
19
|
-
}]
|
|
20
|
-
},
|
|
21
|
-
transformIgnorePatterns: [
|
|
22
|
-
'node_modules/(?!(.pnpm|nanoid|@litepos|dexie|lodash-es)/)', // Transform ESM packages
|
|
23
|
-
],
|
|
24
|
-
testMatch: [
|
|
25
|
-
'<rootDir>/src/test/**/*.test.ts'
|
|
26
|
-
],
|
|
27
|
-
moduleNameMapper: {
|
|
28
|
-
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/scripts/testMock.cjs',
|
|
29
|
-
'\\.(css|less)$': '<rootDir>/scripts/testMock.cjs',
|
|
30
|
-
'^nanoid$': '<rootDir>/src/test/__mocks__/nanoid.js',
|
|
31
|
-
'^dexie$': '<rootDir>/src/test/__mocks__/dexie.js',
|
|
32
|
-
'^@awesome-cordova-plugins/network-interface$': '<rootDir>/src/test/__mocks__/@awesome-cordova-plugins/network-interface.js',
|
|
33
|
-
'^canvas$': '<rootDir>/scripts/testMock.cjs'
|
|
34
|
-
},
|
|
35
|
-
moduleFileExtensions: ['web.js', 'js', 'web.ts', 'ts', 'web.tsx', 'tsx', 'json', 'web.jsx', 'jsx', 'node'],
|
|
36
|
-
};
|
|
1
|
+
// jest.config.js
|
|
2
|
+
module.exports = {
|
|
3
|
+
preset: 'ts-jest',
|
|
4
|
+
testEnvironment: 'jsdom',
|
|
5
|
+
setupFilesAfterEnv: ['<rootDir>/jest.setup.cjs'],
|
|
6
|
+
transform: {
|
|
7
|
+
'^.+\\.ts$': ['ts-jest', {
|
|
8
|
+
tsconfig: {
|
|
9
|
+
module: 'commonjs',
|
|
10
|
+
esModuleInterop: true,
|
|
11
|
+
}
|
|
12
|
+
}],
|
|
13
|
+
'^.+\\.(mjs|js)$': ['ts-jest', {
|
|
14
|
+
tsconfig: {
|
|
15
|
+
allowJs: true,
|
|
16
|
+
module: 'commonjs',
|
|
17
|
+
esModuleInterop: true,
|
|
18
|
+
}
|
|
19
|
+
}]
|
|
20
|
+
},
|
|
21
|
+
transformIgnorePatterns: [
|
|
22
|
+
'node_modules/(?!(.pnpm|nanoid|@litepos|dexie|lodash-es)/)', // Transform ESM packages
|
|
23
|
+
],
|
|
24
|
+
testMatch: [
|
|
25
|
+
'<rootDir>/src/test/**/*.test.ts'
|
|
26
|
+
],
|
|
27
|
+
moduleNameMapper: {
|
|
28
|
+
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/scripts/testMock.cjs',
|
|
29
|
+
'\\.(css|less)$': '<rootDir>/scripts/testMock.cjs',
|
|
30
|
+
'^nanoid$': '<rootDir>/src/test/__mocks__/nanoid.js',
|
|
31
|
+
'^dexie$': '<rootDir>/src/test/__mocks__/dexie.js',
|
|
32
|
+
'^@awesome-cordova-plugins/network-interface$': '<rootDir>/src/test/__mocks__/@awesome-cordova-plugins/network-interface.js',
|
|
33
|
+
'^canvas$': '<rootDir>/scripts/testMock.cjs'
|
|
34
|
+
},
|
|
35
|
+
moduleFileExtensions: ['web.js', 'js', 'web.ts', 'ts', 'web.tsx', 'tsx', 'json', 'web.jsx', 'jsx', 'node'],
|
|
36
|
+
};
|
package/jest.setup.cjs
CHANGED
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
// Jest setup file to polyfill File and Blob APIs
|
|
2
|
-
// This ensures consistent behavior across different Node.js versions and test environments
|
|
3
|
-
|
|
4
|
-
// Polyfill Blob.text() if not available
|
|
5
|
-
if (typeof Blob !== 'undefined' && !Blob.prototype.text) {
|
|
6
|
-
Blob.prototype.text = async function() {
|
|
7
|
-
const reader = new FileReader();
|
|
8
|
-
return new Promise((resolve, reject) => {
|
|
9
|
-
reader.onload = () => resolve(reader.result);
|
|
10
|
-
reader.onerror = reject;
|
|
11
|
-
reader.readAsText(this);
|
|
12
|
-
});
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Polyfill FileReader if not available
|
|
17
|
-
if (typeof FileReader === 'undefined') {
|
|
18
|
-
global.FileReader = class FileReader {
|
|
19
|
-
result = null;
|
|
20
|
-
error = null;
|
|
21
|
-
onload = null;
|
|
22
|
-
onerror = null;
|
|
23
|
-
|
|
24
|
-
readAsText(blob) {
|
|
25
|
-
try {
|
|
26
|
-
// For our polyfilled Blob
|
|
27
|
-
if (blob.parts) {
|
|
28
|
-
this.result = blob.parts.join('');
|
|
29
|
-
} else if (typeof blob === 'string') {
|
|
30
|
-
this.result = blob;
|
|
31
|
-
} else {
|
|
32
|
-
this.result = String(blob);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (this.onload) {
|
|
36
|
-
setTimeout(() => this.onload({ target: this }), 0);
|
|
37
|
-
}
|
|
38
|
-
} catch (err) {
|
|
39
|
-
this.error = err;
|
|
40
|
-
if (this.onerror) {
|
|
41
|
-
setTimeout(() => this.onerror({ target: this }), 0);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Polyfill Blob if not available (for Node.js < 18)
|
|
49
|
-
if (typeof Blob === 'undefined') {
|
|
50
|
-
global.Blob = class Blob {
|
|
51
|
-
constructor(parts = [], options = {}) {
|
|
52
|
-
this.parts = parts;
|
|
53
|
-
this.type = options.type || '';
|
|
54
|
-
this.size = parts.reduce((acc, part) => {
|
|
55
|
-
return acc + (typeof part === 'string' ? part.length : part.length || 0);
|
|
56
|
-
}, 0);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async text() {
|
|
60
|
-
return this.parts.join('');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async arrayBuffer() {
|
|
64
|
-
const text = await this.text();
|
|
65
|
-
const buffer = Buffer.from(text);
|
|
66
|
-
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Polyfill File if not available (for Node.js < 18)
|
|
72
|
-
if (typeof File === 'undefined') {
|
|
73
|
-
global.File = class File extends global.Blob {
|
|
74
|
-
constructor(bits, name, options = {}) {
|
|
75
|
-
super(bits, options);
|
|
76
|
-
this.name = name;
|
|
77
|
-
this.lastModified = options.lastModified || Date.now();
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
}
|
|
1
|
+
// Jest setup file to polyfill File and Blob APIs
|
|
2
|
+
// This ensures consistent behavior across different Node.js versions and test environments
|
|
3
|
+
|
|
4
|
+
// Polyfill Blob.text() if not available
|
|
5
|
+
if (typeof Blob !== 'undefined' && !Blob.prototype.text) {
|
|
6
|
+
Blob.prototype.text = async function() {
|
|
7
|
+
const reader = new FileReader();
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
reader.onload = () => resolve(reader.result);
|
|
10
|
+
reader.onerror = reject;
|
|
11
|
+
reader.readAsText(this);
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Polyfill FileReader if not available
|
|
17
|
+
if (typeof FileReader === 'undefined') {
|
|
18
|
+
global.FileReader = class FileReader {
|
|
19
|
+
result = null;
|
|
20
|
+
error = null;
|
|
21
|
+
onload = null;
|
|
22
|
+
onerror = null;
|
|
23
|
+
|
|
24
|
+
readAsText(blob) {
|
|
25
|
+
try {
|
|
26
|
+
// For our polyfilled Blob
|
|
27
|
+
if (blob.parts) {
|
|
28
|
+
this.result = blob.parts.join('');
|
|
29
|
+
} else if (typeof blob === 'string') {
|
|
30
|
+
this.result = blob;
|
|
31
|
+
} else {
|
|
32
|
+
this.result = String(blob);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (this.onload) {
|
|
36
|
+
setTimeout(() => this.onload({ target: this }), 0);
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
this.error = err;
|
|
40
|
+
if (this.onerror) {
|
|
41
|
+
setTimeout(() => this.onerror({ target: this }), 0);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Polyfill Blob if not available (for Node.js < 18)
|
|
49
|
+
if (typeof Blob === 'undefined') {
|
|
50
|
+
global.Blob = class Blob {
|
|
51
|
+
constructor(parts = [], options = {}) {
|
|
52
|
+
this.parts = parts;
|
|
53
|
+
this.type = options.type || '';
|
|
54
|
+
this.size = parts.reduce((acc, part) => {
|
|
55
|
+
return acc + (typeof part === 'string' ? part.length : part.length || 0);
|
|
56
|
+
}, 0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async text() {
|
|
60
|
+
return this.parts.join('');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async arrayBuffer() {
|
|
64
|
+
const text = await this.text();
|
|
65
|
+
const buffer = Buffer.from(text);
|
|
66
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Polyfill File if not available (for Node.js < 18)
|
|
72
|
+
if (typeof File === 'undefined') {
|
|
73
|
+
global.File = class File extends global.Blob {
|
|
74
|
+
constructor(bits, name, options = {}) {
|
|
75
|
+
super(bits, options);
|
|
76
|
+
this.name = name;
|
|
77
|
+
this.lastModified = options.lastModified || Date.now();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# 01 - 整体架构
|
|
2
|
+
|
|
3
|
+
> AI 上下文索引:服务工厂、依赖注入、Dexie数据库、Axios HTTP、服务层次、事件驱动
|
|
4
|
+
|
|
5
|
+
## 架构概述
|
|
6
|
+
|
|
7
|
+
本项目是 **核心服务库**,非前端应用。采用服务导向架构,通过工厂模式进行依赖注入。
|
|
8
|
+
|
|
9
|
+
## 入口点
|
|
10
|
+
|
|
11
|
+
**主入口:** `src/index.ts`
|
|
12
|
+
- Barrel 导出所有公共 API
|
|
13
|
+
- 导出服务工厂、领域模型、工具函数
|
|
14
|
+
|
|
15
|
+
**服务工厂:** `src/service.factory.ts`
|
|
16
|
+
- 初始化 Dexie 数据库 (28 张表)
|
|
17
|
+
- 创建 Axios HTTP 客户端
|
|
18
|
+
- 按需实例化领域服务
|
|
19
|
+
- 管理数据库连接状态
|
|
20
|
+
|
|
21
|
+
## 服务层次结构
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
IAppCoreService<T> # 读操作接口
|
|
25
|
+
│
|
|
26
|
+
├── IAppLocalService<T> # 本地 CRUD (无远程)
|
|
27
|
+
│
|
|
28
|
+
└── IAppRemoteService<T> # 远程 API CRUD
|
|
29
|
+
│
|
|
30
|
+
├── addOne() → PUT
|
|
31
|
+
├── updateOne() → PATCH
|
|
32
|
+
├── updateMany() → PATCH
|
|
33
|
+
└── deleteOne() → DELETE (软删除)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 数据库架构
|
|
37
|
+
|
|
38
|
+
**技术:** Dexie v3.2.4 (IndexedDB 包装器)
|
|
39
|
+
**版本:** v28
|
|
40
|
+
**表数量:** 28 张
|
|
41
|
+
|
|
42
|
+
### 核心表
|
|
43
|
+
|
|
44
|
+
| 表名 | 索引 | 用途 |
|
|
45
|
+
|------|------|------|
|
|
46
|
+
| items | uid, name, barcode, codename, sequence | 商品 |
|
|
47
|
+
| sections | uid, sequence | 区域 |
|
|
48
|
+
| section_items | uid, sequence | 桌位 |
|
|
49
|
+
| categories | uid, sequence | 分类 |
|
|
50
|
+
| invoices | uid, type, status, table_uid | 发票 |
|
|
51
|
+
| employees | uid, role_uid | 员工 |
|
|
52
|
+
| payment_methods | uid, is_cash | 支付方式 |
|
|
53
|
+
| printers | uid | 打印机 |
|
|
54
|
+
| print_jobs | uid, status, printer_type, device_uid_to_print | 打印任务 |
|
|
55
|
+
| configs | uid | 配置 |
|
|
56
|
+
| shifts | uid, status | 班次 |
|
|
57
|
+
| tills | uid, employee_uid | 收银机 |
|
|
58
|
+
|
|
59
|
+
### 基础索引模式
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
const baseSchema = 'uid, updated_at, created_at, deleted_at, _timestamp, id_in_server, created_at_timestamp';
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 事件驱动模式
|
|
66
|
+
|
|
67
|
+
### SignalR 集成
|
|
68
|
+
|
|
69
|
+
**Hub 消息类型:** `src/types/hub.message.type.ts`
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
enum HubMessageType {
|
|
73
|
+
PaynowEvent = 'paynow_event',
|
|
74
|
+
SmoochPaymentEvent = 'smooch_payment_event',
|
|
75
|
+
SyncInvoice = 'sync_invoice',
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Dexie Live Query
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
public liveQuery<T>(expr: () => T): Observable<T> {
|
|
83
|
+
return liveQuery(expr);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
- 提供响应式数据更新
|
|
87
|
+
- 通过 RxJS Observable 集成
|
|
88
|
+
|
|
89
|
+
## Electron Socket 桥接
|
|
90
|
+
|
|
91
|
+
**位置:** `src/libs/electron.socket.ts`
|
|
92
|
+
|
|
93
|
+
**状态机:**
|
|
94
|
+
- CLOSED (0) → OPENING (1) → OPENED (2) → CLOSING (3)
|
|
95
|
+
|
|
96
|
+
**用途:**
|
|
97
|
+
- Electron IPC 通信
|
|
98
|
+
- 硬件控制 (打印机、钱箱)
|
|
99
|
+
|
|
100
|
+
## 插件系统
|
|
101
|
+
|
|
102
|
+
### Handlebars 插件注册
|
|
103
|
+
|
|
104
|
+
**位置:** `src/helpers/helpers.register.ts`
|
|
105
|
+
|
|
106
|
+
用于动态模板渲染:
|
|
107
|
+
- `string.helpers.ts` - 字符串处理
|
|
108
|
+
- `comparison.helpers.ts` - 比较逻辑
|
|
109
|
+
|
|
110
|
+
## 关键文件
|
|
111
|
+
|
|
112
|
+
| 文件路径 | 大小 | 核心职责 |
|
|
113
|
+
|----------|------|---------|
|
|
114
|
+
| `src/service.factory.ts` | 6KB | 服务实例化、数据库初始化 |
|
|
115
|
+
| `src/services/abstract.service.ts` | 8KB | 基础 CRUD 接口 |
|
|
116
|
+
| `src/services/app.service.ts` | 82KB | 领域服务集合 |
|
|
117
|
+
| `src/services/invoice.service.ts` | 107KB | 发票生命周期、计算引擎 |
|
|
118
|
+
| `src/libs/electron.socket.ts` | 3KB | Electron IPC 桥接 |
|
|
119
|
+
| `src/libs/escpos.printer.ts` | 18KB | ESC/POS 打印命令 |
|
|
120
|
+
|
|
121
|
+
## 架构模式
|
|
122
|
+
|
|
123
|
+
1. **服务工厂模式** - 延迟初始化服务
|
|
124
|
+
2. **仓储模式** - AppRemoteService 提供数据访问抽象
|
|
125
|
+
3. **适配器模式** - Dexie + Axios 双重持久化
|
|
126
|
+
4. **观察者模式** - Dexie liveQuery + RxJS
|
|
127
|
+
5. **模板方法模式** - 抽象服务基类
|
|
128
|
+
6. **策略模式** - 多种计算流程 (CalcFlow)
|
|
129
|
+
7. **事件驱动** - SignalR 实时同步
|
|
130
|
+
|
|
131
|
+
## 运行模式
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
enum ServiceMode {
|
|
135
|
+
LOCAL = 'local', // 仅客户端模式
|
|
136
|
+
CLOUD = 'cloud' // 远程 API 同步模式
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**LOCAL:** 仅使用 IndexedDB,无远程调用
|
|
141
|
+
**CLOUD:** 与远程 API 同步,维护本地缓存
|
|
142
|
+
|
|
143
|
+
## 相关文档
|
|
144
|
+
|
|
145
|
+
- [PROJECT_OVERVIEW.md](./PROJECT_OVERVIEW.md) - 项目总览
|
|
146
|
+
- [05_DATA_LAYER.md](./05_DATA_LAYER.md) - 数据层详解
|
|
147
|
+
- [06_CROSS_PLATFORM.md](./06_CROSS_PLATFORM.md) - 跨平台能力
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# 02 - 核心业务逻辑
|
|
2
|
+
|
|
3
|
+
> AI 上下文索引:发票计算、支付处理、打印系统、数据同步、员工认证、权限管理
|
|
4
|
+
|
|
5
|
+
## 1. 发票/账单系统
|
|
6
|
+
|
|
7
|
+
**核心文件:**
|
|
8
|
+
- `src/services/invoice.service.ts` (2800+ 行)
|
|
9
|
+
- `src/types/invoice.type.ts`
|
|
10
|
+
|
|
11
|
+
### 服务层次
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
InvoiceBaseService # 计算引擎
|
|
15
|
+
│
|
|
16
|
+
├── LineOperationService # 行项操作
|
|
17
|
+
│
|
|
18
|
+
└── InvoiceOperationService # 发票生命周期
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### 计算流程
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
1. 行项计算
|
|
25
|
+
├─ 数量 × 单价
|
|
26
|
+
├─ 修改器合计
|
|
27
|
+
└─ 行小计
|
|
28
|
+
|
|
29
|
+
2. 折扣阶段 (CalcFlow)
|
|
30
|
+
├─ AfterSubtotal
|
|
31
|
+
├─ AfterServiceCharge
|
|
32
|
+
└─ AfterGrandTotal
|
|
33
|
+
|
|
34
|
+
3. 服务费
|
|
35
|
+
└─ 仅堂食适用
|
|
36
|
+
|
|
37
|
+
4. 税费计算
|
|
38
|
+
├─ 内含 (inclusive)
|
|
39
|
+
└─ 外加 (exclusive)
|
|
40
|
+
|
|
41
|
+
5. 配送费
|
|
42
|
+
|
|
43
|
+
6. 舍入
|
|
44
|
+
├─ 现金规则
|
|
45
|
+
└─ 非现金规则
|
|
46
|
+
|
|
47
|
+
7. 最终调整
|
|
48
|
+
├─ 积分抵扣
|
|
49
|
+
└─ 零钱计算
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 发票状态流转
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
Open → OnHold/Paying → Paid/Void
|
|
56
|
+
↓
|
|
57
|
+
PayAtCounter, Calibration (特殊状态)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 发票类型
|
|
61
|
+
|
|
62
|
+
| 类型 | 服务费 | 配送费 |
|
|
63
|
+
|------|--------|--------|
|
|
64
|
+
| dine_in | ✓ | ✗ |
|
|
65
|
+
| take_out | ✗ | ✗ |
|
|
66
|
+
| delivery | ✗ | ✓ |
|
|
67
|
+
|
|
68
|
+
### 核心方法
|
|
69
|
+
|
|
70
|
+
| 方法 | 功能 |
|
|
71
|
+
|------|------|
|
|
72
|
+
| `calculate()` | 主计算协调 |
|
|
73
|
+
| `calculateLines()` | 行级小计 |
|
|
74
|
+
| `calculateDiscount()` | 折扣计算 |
|
|
75
|
+
| `calculateTax()` | 税费计算 |
|
|
76
|
+
| `calculateServiceCharge()` | 服务费 |
|
|
77
|
+
| `calculateRounding()` | 舍入处理 |
|
|
78
|
+
| `createInvoice()` | 创建发票 |
|
|
79
|
+
| `payInvoice()` | 支付发票 |
|
|
80
|
+
| `voidInvoice()` | 作废发票 |
|
|
81
|
+
| `sliceInvoice()` | 拆分发票 |
|
|
82
|
+
|
|
83
|
+
## 2. 支付处理
|
|
84
|
+
|
|
85
|
+
**核心文件:**
|
|
86
|
+
- `src/types/payment.type.ts`
|
|
87
|
+
- `src/services/app.service.ts`
|
|
88
|
+
|
|
89
|
+
### 支付实体
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
interface IPayment {
|
|
93
|
+
tender_amount: number; // 实付金额
|
|
94
|
+
change_amount: number; // 找零
|
|
95
|
+
payment_method: IPaymentMethod;
|
|
96
|
+
payment_method_history: []; // 变更历史
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface IPaymentMethod {
|
|
100
|
+
is_cash: boolean;
|
|
101
|
+
enable_drawer: boolean; // 钱箱支持
|
|
102
|
+
enable_receipt: boolean; // 小票打印
|
|
103
|
+
is_integrated: boolean; // 集成支付
|
|
104
|
+
enable_credit_validation: boolean;
|
|
105
|
+
enable_customer_validation: boolean;
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 支付状态
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
enum PaymentStatus {
|
|
113
|
+
payment_started,
|
|
114
|
+
payment_completed,
|
|
115
|
+
payment_failed,
|
|
116
|
+
payment_cancelled
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 支付操作
|
|
121
|
+
|
|
122
|
+
- `addPayment()` - 添加支付
|
|
123
|
+
- `removePayment()` - 移除支付
|
|
124
|
+
- `changePayments()` - 更换支付方式
|
|
125
|
+
- `syncStandardPaymentMethods()` - 同步支付方式
|
|
126
|
+
|
|
127
|
+
## 3. 打印系统
|
|
128
|
+
|
|
129
|
+
**核心文件:**
|
|
130
|
+
- `src/services/printer.service.ts`
|
|
131
|
+
- `src/types/printer.type.ts`
|
|
132
|
+
- `src/libs/escpos.printer.ts`
|
|
133
|
+
|
|
134
|
+
### 打印机类型
|
|
135
|
+
|
|
136
|
+
| 类型 | 用途 |
|
|
137
|
+
|------|------|
|
|
138
|
+
| ReceiptPrinter | 收据打印 |
|
|
139
|
+
| KitchenPrinter | 厨房小票 |
|
|
140
|
+
| KioskPrinter | 自助终端 |
|
|
141
|
+
| LabelPrinter | 标签打印 |
|
|
142
|
+
| OrderPrinter | 订单打印 |
|
|
143
|
+
| CheckPrinter | 账单打印 |
|
|
144
|
+
|
|
145
|
+
### 打印服务能力
|
|
146
|
+
|
|
147
|
+
- 物理打印机发现
|
|
148
|
+
- 打印机配对
|
|
149
|
+
- 网络扫描 (子网探测)
|
|
150
|
+
- 钱箱控制
|
|
151
|
+
- 模板渲染 (Handlebars)
|
|
152
|
+
- ESC/POS 命令生成
|
|
153
|
+
|
|
154
|
+
### 打印任务队列
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
interface IPrintJob {
|
|
158
|
+
printer_uid: string;
|
|
159
|
+
connection_type: PrinterConnections;
|
|
160
|
+
protocol: PrinterProtocols;
|
|
161
|
+
status: PrintJobStatus; // queued, printed, failed, expired
|
|
162
|
+
device_uid_to_print: string;
|
|
163
|
+
save_only: boolean;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## 4. 数据同步
|
|
168
|
+
|
|
169
|
+
**核心文件:**
|
|
170
|
+
- `src/services/abstract.service.ts`
|
|
171
|
+
- `src/services/app.service.ts`
|
|
172
|
+
|
|
173
|
+
### 远程服务 API
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
PUT /api/v5/merchants/{merchant_uid}/{module}/{method} # 创建
|
|
177
|
+
PATCH /api/v5/merchants/{merchant_uid}/{module}/{id} # 更新
|
|
178
|
+
DELETE /api/v5/merchants/{merchant_uid}/{module}/{id} # 删除
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 同步策略
|
|
182
|
+
|
|
183
|
+
- **双键设计:** `uid` (本地 nanoid) + `id_in_server` (远程 ID)
|
|
184
|
+
- **软删除:** `deleted_at` 字段标记
|
|
185
|
+
- **冲突检测:** `outdatedInvoice`, `outdatedSectionItem`
|
|
186
|
+
|
|
187
|
+
### 同步操作
|
|
188
|
+
|
|
189
|
+
| 操作 | 方法 |
|
|
190
|
+
|------|------|
|
|
191
|
+
| 发票数据同步 | `syncInvoiceData()` |
|
|
192
|
+
| 支付方式同步 | `syncStandardPaymentMethods()` |
|
|
193
|
+
| 报表同步 | `syncStandardReports()` |
|
|
194
|
+
|
|
195
|
+
## 5. 员工认证
|
|
196
|
+
|
|
197
|
+
**核心文件:**
|
|
198
|
+
- `src/types/employee.type.ts`
|
|
199
|
+
- `src/services/app.service.ts`
|
|
200
|
+
|
|
201
|
+
### 认证方法
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// 登录验证 (bcryptjs, 12 rounds)
|
|
205
|
+
await bcryptjs.compare(password, user.password)
|
|
206
|
+
|
|
207
|
+
// 密码修改
|
|
208
|
+
await bcryptjs.hash(newPassword, 12)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### 员工实体
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
interface IEmployee {
|
|
215
|
+
user_uid: string;
|
|
216
|
+
name: string;
|
|
217
|
+
email: string;
|
|
218
|
+
password: string; // hashed
|
|
219
|
+
role_uid: string;
|
|
220
|
+
language: string;
|
|
221
|
+
can_login: boolean;
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### 认证操作
|
|
226
|
+
|
|
227
|
+
- `login()` - 员工登录
|
|
228
|
+
- `createEmployee()` - 创建员工
|
|
229
|
+
- `changePassword()` - 修改密码
|
|
230
|
+
|
|
231
|
+
## 6. 权限管理
|
|
232
|
+
|
|
233
|
+
**核心文件:**
|
|
234
|
+
- `src/types/role.type.ts`
|
|
235
|
+
|
|
236
|
+
### 角色实体
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
interface IEmployeeRole extends IAppBaseModel {
|
|
240
|
+
permissions: string[]; // 权限字符串数组
|
|
241
|
+
is_default: boolean;
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### 权限设计
|
|
246
|
+
|
|
247
|
+
- 字符串形式权限
|
|
248
|
+
- 数组存储多权限
|
|
249
|
+
- 通过 `role_uid` 关联员工
|
|
250
|
+
- 服务端验证 (非本地)
|
|
251
|
+
|
|
252
|
+
## 横切关注点
|
|
253
|
+
|
|
254
|
+
### 配置系统 (ConfigService)
|
|
255
|
+
|
|
256
|
+
包含 20+ 子系统配置:
|
|
257
|
+
- General, Till, Report, Receipt, Kitchen
|
|
258
|
+
- Tax, Rounding, Charges, Currency
|
|
259
|
+
- Inventory, PAX, CRM, Loyalty Program
|
|
260
|
+
- Online Order, Kiosk, Order Display
|
|
261
|
+
|
|
262
|
+
### 订单显示系统 (ODS)
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// 订单状态流转
|
|
266
|
+
Preparing → ReadyForPickup
|
|
267
|
+
|
|
268
|
+
// 操作
|
|
269
|
+
toPickUp(), toPreparing(), undo()
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### 工具函数
|
|
273
|
+
|
|
274
|
+
- `preciseRound()` - 精确舍入 (2 位小数)
|
|
275
|
+
- `humanizedData()` - 日期格式化
|
|
276
|
+
- CSV 导入/导出
|
|
277
|
+
|
|
278
|
+
## 关键文件汇总
|
|
279
|
+
|
|
280
|
+
| 文件 | 大小 | 职责 |
|
|
281
|
+
|------|------|------|
|
|
282
|
+
| invoice.service.ts | 107KB | 发票完整生命周期 |
|
|
283
|
+
| app.service.ts | 82KB | 通用 CRUD、配置、支付、报表 |
|
|
284
|
+
| abstract.service.ts | 8KB | 服务基类 |
|
|
285
|
+
| printer.service.ts | 9KB | 打印机发现与管理 |
|
|
286
|
+
| ods.service.ts | 4KB | 订单显示操作 |
|
|
287
|
+
|
|
288
|
+
## 相关文档
|
|
289
|
+
|
|
290
|
+
- [01_ARCHITECTURE.md](./01_ARCHITECTURE.md) - 整体架构
|
|
291
|
+
- [05_DATA_LAYER.md](./05_DATA_LAYER.md) - 数据层
|
|
292
|
+
- [06_CROSS_PLATFORM.md](./06_CROSS_PLATFORM.md) - 跨平台
|