Przeglądaj źródła

feat: 拉迷软装 Parse Server 后端代码同步

- server.ts: Express + Parse Server 模式B入口
- api/routes.ts: API桶文件,挂载 order/receipt/finance/store/log/auth
- api/module/auth: 登录认证接口(支持email/phone/name)
- api/module/order: 订单CRUD接口
- api/module/receipt: 回执管理接口
- api/module/finance: 财务核销接口
- api/module/store: 门店管理接口
- api/module/log: 操作日志接口
- cloud/main.js: Parse Cloud Code(hello/orderStats/钩子)
- scripts/seed.ts: 种子数据脚本
- .gitignore: 排除日志目录和环境变量文件

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
cb 2 miesięcy temu
rodzic
commit
1ae7e3874e

+ 5 - 0
.gitignore

@@ -18,6 +18,11 @@ Desktop.ini
 .env
 .env.local
 .env.*.local
+.env.production
+.env.development
+
+# Logs
+logs/
 
 # Runtime
 *.pid

+ 116 - 0
api/module/auth/routes.ts

@@ -0,0 +1,116 @@
+import express from 'express';
+const router = express.Router();
+
+// POST /api/auth/login — 支持 email/phone/name 登录
+router.post('/login', async (req, res) => {
+  try {
+    const Parse = (globalThis as any).Parse;
+    const { identifier, password } = req.body;
+
+    if (!identifier || !password) {
+      return res.status(400).json({ success: false, error: '缺少 identifier 或 password' });
+    }
+
+    // 1. 先尝试直接用 identifier 作为 username 登录
+    try {
+      const user = await Parse.User.logIn(identifier, password);
+      return res.json({
+        success: true,
+        data: {
+          objectId: user.id,
+          username: user.get('username'),
+          email: user.get('email'),
+          name: user.get('name'),
+          role: user.get('role'),
+          department: user.get('department'),
+          phone: user.get('phone'),
+          sessionToken: user.getSessionToken(),
+        },
+      });
+    } catch { /* 继续尝试其他方式 */ }
+
+    // 2. 通过 email 查找
+    const q1 = new Parse.Query(Parse.User);
+    q1.equalTo('email', identifier);
+    let user = await q1.first({ useMasterKey: true });
+
+    // 3. 通过 phone 查找
+    if (!user) {
+      const q2 = new Parse.Query(Parse.User);
+      q2.equalTo('phone', identifier);
+      user = await q2.first({ useMasterKey: true });
+    }
+
+    // 4. 通过 name 查找
+    if (!user) {
+      const q3 = new Parse.Query(Parse.User);
+      q3.equalTo('name', identifier);
+      user = await q3.first({ useMasterKey: true });
+    }
+
+    if (!user) {
+      return res.status(401).json({ success: false, error: '账户不存在' });
+    }
+
+    // 用找到的 username 尝试登录
+    try {
+      const loggedIn = await Parse.User.logIn(user.get('username'), password);
+      return res.json({
+        success: true,
+        data: {
+          objectId: loggedIn.id,
+          username: loggedIn.get('username'),
+          email: loggedIn.get('email'),
+          name: loggedIn.get('name'),
+          role: loggedIn.get('role'),
+          department: loggedIn.get('department'),
+          phone: loggedIn.get('phone'),
+          sessionToken: loggedIn.getSessionToken(),
+        },
+      });
+    } catch {
+      return res.status(401).json({ success: false, error: '密码错误或账户已停用' });
+    }
+  } catch (e: any) {
+    res.status(500).json({ success: false, error: e.message });
+  }
+});
+
+// GET /api/auth/me — 获取当前用户
+router.get('/me', async (req, res) => {
+  try {
+    const token = req.headers.authorization?.replace('Bearer ', '') || '';
+    if (!token) {
+      return res.status(401).json({ success: false, error: '未登录' });
+    }
+    const Parse = (globalThis as any).Parse;
+    const q = new Parse.Query('_Session');
+    q.equalTo('sessionToken', token);
+    const session = await q.first({ useMasterKey: true });
+    if (!session) {
+      return res.status(401).json({ success: false, error: 'token 已失效' });
+    }
+    const user = session.get('user');
+    if (user) {
+      await user.fetch({ useMasterKey: true });
+      res.json({
+        success: true,
+        data: {
+          objectId: user.id,
+          username: user.get('username'),
+          email: user.get('email'),
+          name: user.get('name'),
+          role: user.get('role'),
+          department: user.get('department'),
+          phone: user.get('phone'),
+        },
+      });
+    } else {
+      res.status(401).json({ success: false, error: '用户不存在' });
+    }
+  } catch (e: any) {
+    res.status(500).json({ success: false, error: e.message });
+  }
+});
+
+export default router;

+ 45 - 0
api/module/finance/routes.ts

@@ -0,0 +1,45 @@
+import express from 'express';
+const router = express.Router();
+
+// GET /api/finance — 财务记录列表
+router.get('/', async (req, res) => {
+  try {
+    const Parse = (globalThis as any).Parse;
+    const q = new Parse.Query('FinanceRecord');
+    q.descending('orderDate');
+    q.limit(100);
+
+    const { status } = req.query;
+    if (status && status !== 'all') {
+      q.equalTo('status', status);
+    }
+
+    const list = await q.find({ useMasterKey: true });
+    const data = list.map((r: any) => r.toJSON());
+    res.json({ success: true, data });
+  } catch (e: any) {
+    res.status(500).json({ success: false, error: e.message });
+  }
+});
+
+// POST /api/finance/verify — 核销确认
+router.post('/verify', async (req, res) => {
+  try {
+    const Parse = (globalThis as any).Parse;
+    const { id } = req.body;
+    if (!id) {
+      return res.status(400).json({ success: false, error: '缺少 id' });
+    }
+
+    const q = new Parse.Query('FinanceRecord');
+    const record = await q.get(id, { useMasterKey: true });
+    record.set('verified', true);
+    const saved = await record.save(null, { useMasterKey: true });
+
+    res.json({ success: true, data: saved.toJSON() });
+  } catch (e: any) {
+    res.status(500).json({ success: false, error: e.message });
+  }
+});
+
+export default router;

+ 52 - 0
api/module/log/routes.ts

@@ -0,0 +1,52 @@
+import express from 'express';
+const router = express.Router();
+
+// GET /api/log — 获取操作日志
+router.get('/', async (_req, res) => {
+  try {
+    const Parse = (globalThis as any).Parse;
+    const q = new Parse.Query('OperationLog');
+    q.descending('createdAt');
+    q.limit(500);
+    const list = await q.find({ useMasterKey: true });
+    const data = list.map((l: any) => ({
+      id: l.id,
+      action: l.get('action'),
+      objectType: l.get('objectType'),
+      objectId: l.get('objectId'),
+      operatorId: l.get('operatorId'),
+      operatorName: l.get('operatorName'),
+      details: l.get('details'),
+      ip: l.get('ip'),
+      status: l.get('status'),
+      timestamp: l.get('createdAt'),
+    }));
+    res.json(data);
+  } catch (e: any) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// POST /api/log — 添加操作日志
+router.post('/', async (req, res) => {
+  try {
+    const Parse = (globalThis as any).Parse;
+    const entry = req.body;
+    const OperationLog = Parse.Object.extend('OperationLog');
+    const obj = new OperationLog();
+    obj.set('action', entry.action || '');
+    obj.set('objectType', entry.objectType || '');
+    obj.set('objectId', entry.objectId || '');
+    obj.set('operatorId', entry.operatorId || '');
+    obj.set('operatorName', entry.operatorName || '');
+    obj.set('details', entry.details || '');
+    obj.set('ip', entry.ip || '');
+    obj.set('status', entry.status || 'success');
+    const saved = await obj.save(null, { useMasterKey: true });
+    res.json({ ok: true, id: saved.id || '' });
+  } catch (e: any) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+export default router;

+ 77 - 0
api/module/order/routes.ts

@@ -0,0 +1,77 @@
+import express from 'express';
+const router = express.Router();
+
+// GET /api/order — 查询订单列表
+router.get('/', async (req, res) => {
+  try {
+    const Parse = (globalThis as any).Parse;
+    const q = new Parse.Query('Order');
+    q.notEqualTo('isDeleted', true);
+    q.descending('createdAt');
+    q.limit(100);
+
+    const { status, search } = req.query;
+    if (status && status !== 'all') {
+      q.equalTo('status', status);
+    }
+    if (search) {
+      q.contains('customerName', search as string);
+    }
+
+    const list = await q.find({ useMasterKey: true });
+    const data = list.map((o: any) => o.toJSON());
+    res.json({ success: true, data });
+  } catch (e: any) {
+    res.status(500).json({ success: false, error: e.message });
+  }
+});
+
+// GET /api/order/:id — 查询单个订单
+router.get('/:id', async (req, res) => {
+  try {
+    const Parse = (globalThis as any).Parse;
+    const q = new Parse.Query('Order');
+    q.equalTo('objectId', req.params.id);
+    const order = await q.first({ useMasterKey: true });
+    if (!order) {
+      return res.status(404).json({ success: false, error: '订单不存在' });
+    }
+    res.json({ success: true, data: order.toJSON() });
+  } catch (e: any) {
+    res.status(500).json({ success: false, error: e.message });
+  }
+});
+
+// POST /api/order — 创建/更新订单
+router.post('/', async (req, res) => {
+  try {
+    const Parse = (globalThis as any).Parse;
+    const { objectId, ...fields } = req.body;
+
+    let obj: any;
+    if (objectId) {
+      // 更新已有订单
+      const q = new Parse.Query('Order');
+      obj = await q.get(objectId, { useMasterKey: true });
+    } else {
+      // 创建新订单
+      const Order = Parse.Object.extend('Order');
+      obj = new Order();
+    }
+
+    // 设置字段
+    Object.entries(fields).forEach(([key, value]) => {
+      obj.set(key, value);
+    });
+    if (!objectId) {
+      obj.set('isDeleted', false);
+    }
+
+    const saved = await obj.save(null, { useMasterKey: true });
+    res.json({ success: true, data: saved.toJSON() });
+  } catch (e: any) {
+    res.status(500).json({ success: false, error: e.message });
+  }
+});
+
+export default router;

+ 61 - 0
api/module/receipt/routes.ts

@@ -0,0 +1,61 @@
+import express from 'express';
+const router = express.Router();
+
+// GET /api/receipt — 查询回执列表
+router.get('/', async (req, res) => {
+  try {
+    const Parse = (globalThis as any).Parse;
+    const q = new Parse.Query('Receipt');
+    q.descending('createdAt');
+    q.limit(50);
+    const list = await q.find({ useMasterKey: true });
+    const data = list.map((r: any) => r.toJSON());
+    res.json({ success: true, data });
+  } catch (e: any) {
+    res.status(500).json({ success: false, error: e.message });
+  }
+});
+
+// GET /api/receipt/:id — 查询单个回执
+router.get('/:id', async (req, res) => {
+  try {
+    const Parse = (globalThis as any).Parse;
+    const q = new Parse.Query('Receipt');
+    q.equalTo('objectId', req.params.id);
+    const receipt = await q.first({ useMasterKey: true });
+    if (!receipt) {
+      return res.status(404).json({ success: false, error: '回执不存在' });
+    }
+    res.json({ success: true, data: receipt.toJSON() });
+  } catch (e: any) {
+    res.status(500).json({ success: false, error: e.message });
+  }
+});
+
+// POST /api/receipt — 创建/更新回执
+router.post('/', async (req, res) => {
+  try {
+    const Parse = (globalThis as any).Parse;
+    const { objectId, ...fields } = req.body;
+
+    let obj: any;
+    if (objectId) {
+      const q = new Parse.Query('Receipt');
+      obj = await q.get(objectId, { useMasterKey: true });
+    } else {
+      const Receipt = Parse.Object.extend('Receipt');
+      obj = new Receipt();
+    }
+
+    Object.entries(fields).forEach(([key, value]) => {
+      obj.set(key, value);
+    });
+
+    const saved = await obj.save(null, { useMasterKey: true });
+    res.json({ success: true, data: saved.toJSON() });
+  } catch (e: any) {
+    res.status(500).json({ success: false, error: e.message });
+  }
+});
+
+export default router;

+ 92 - 0
api/module/store/routes.ts

@@ -0,0 +1,92 @@
+import express from 'express';
+const router = express.Router();
+
+// GET /api/store — 获取门店列表
+router.get('/', async (_req, res) => {
+  try {
+    const Parse = (globalThis as any).Parse;
+    const q = new Parse.Query('StoreConfig');
+    q.ascending('code');
+    q.limit(100);
+    const list = await q.find({ useMasterKey: true });
+    const data = list.map((s: any) => ({
+      id: s.id,
+      name: s.get('name'),
+      code: s.get('code'),
+    }));
+    res.json(data);
+  } catch (e: any) {
+    // 如果 StoreConfig 表还不存在,返回默认列表
+    res.json([
+      { id: 's01', name: '真北北馆一队', code: 'ZB1' },
+      { id: 's02', name: '真北北馆二队', code: 'ZB2' },
+      { id: 's03', name: '真北南馆一队', code: 'ZN1' },
+      { id: 's04', name: '真北南馆二队', code: 'ZN2' },
+      { id: 's05', name: '浦东一队', code: 'PD1' },
+      { id: 's06', name: '浦东二队', code: 'PD2' },
+      { id: 's07', name: '徐汇一队', code: 'XH1' },
+      { id: 's08', name: '徐汇二队', code: 'XH2' },
+      { id: 's09', name: '闵行一队', code: 'MH1' },
+      { id: 's10', name: '闵行二队', code: 'MH2' },
+      { id: 's11', name: '杨浦一队', code: 'YP1' },
+      { id: 's12', name: '杨浦二队', code: 'YP2' },
+      { id: 's13', name: '长宁一队', code: 'CN1' },
+      { id: 's14', name: '长宁二队', code: 'CN2' },
+      { id: 's15', name: '虹口一队', code: 'HK1' },
+      { id: 's16', name: '虹口二队', code: 'HK2' },
+      { id: 's17', name: '松江一队', code: 'SJ1' },
+      { id: 's18', name: '松江二队', code: 'SJ2' },
+      { id: 's19', name: '家装渠道部', code: 'JZ1' },
+    ]);
+  }
+});
+
+// POST /api/store — 新增门店
+router.post('/', async (req, res) => {
+  try {
+    const Parse = (globalThis as any).Parse;
+    const { name, code } = req.body;
+    if (!name || !code) {
+      return res.status(400).json({ success: false, error: 'name 和 code 不能为空' });
+    }
+    const StoreConfig = Parse.Object.extend('StoreConfig');
+    const obj = new StoreConfig();
+    obj.set('name', name);
+    obj.set('code', code);
+    const saved = await obj.save(null, { useMasterKey: true });
+    res.json({ success: true, data: { id: saved.id, name, code } });
+  } catch (e: any) {
+    res.status(500).json({ success: false, error: e.message });
+  }
+});
+
+// PUT /api/store/:id — 更新门店
+router.put('/:id', async (req, res) => {
+  try {
+    const Parse = (globalThis as any).Parse;
+    const q = new Parse.Query('StoreConfig');
+    const obj = await q.get(req.params.id, { useMasterKey: true });
+    const { name, code } = req.body;
+    if (name) obj.set('name', name);
+    if (code) obj.set('code', code);
+    await obj.save(null, { useMasterKey: true });
+    res.json({ success: true });
+  } catch (e: any) {
+    res.status(500).json({ success: false, error: e.message });
+  }
+});
+
+// DELETE /api/store/:id — 删除门店
+router.delete('/:id', async (req, res) => {
+  try {
+    const Parse = (globalThis as any).Parse;
+    const q = new Parse.Query('StoreConfig');
+    const obj = await q.get(req.params.id, { useMasterKey: true });
+    await obj.destroy({ useMasterKey: true });
+    res.json({ success: true });
+  } catch (e: any) {
+    res.status(500).json({ success: false, error: e.message });
+  }
+});
+
+export default router;

+ 35 - 0
api/routes.ts

@@ -0,0 +1,35 @@
+import express from 'express';
+const router = express.Router();
+
+// 引入各业务模块路由
+import orderRoutes from './module/order/routes.js';
+import receiptRoutes from './module/receipt/routes.js';
+import financeRoutes from './module/finance/routes.js';
+import storeRoutes from './module/store/routes.js';
+import logRoutes from './module/log/routes.js';
+import authRoutes from './module/auth/routes.js';
+
+// 挂载子路由
+router.use('/order', orderRoutes);
+router.use('/receipt', receiptRoutes);
+router.use('/finance', financeRoutes);
+router.use('/store', storeRoutes);
+router.use('/log', logRoutes);
+router.use('/auth', authRoutes);
+
+// GET /api — 索引
+router.get('/', (req, res) => {
+  res.json({
+    success: true,
+    routes: [
+      '/api/order',
+      '/api/receipt',
+      '/api/finance',
+      '/api/store',
+      '/api/log',
+      '/api/auth',
+    ],
+  });
+});
+
+export default router;

+ 31 - 0
cloud/main.js

@@ -0,0 +1,31 @@
+// Parse Cloud Code — 拉迷软装一体化管理系统
+
+// 健康检查
+Parse.Cloud.define('hello', req => 'Hello from 拉迷软装 Parse Server!');
+
+// 获取订单统计
+Parse.Cloud.define('orderStats', async (req) => {
+  const Parse = globalThis.Parse || require('parse/node');
+  const q = new Parse.Query('Order');
+  q.notEqualTo('isDeleted', true);
+  const total = await q.count({ useMasterKey: true });
+
+  const pendingQ = new Parse.Query('Order');
+  pendingQ.equalTo('status', 'pending');
+  pendingQ.notEqualTo('isDeleted', true);
+  const pending = await pendingQ.count({ useMasterKey: true });
+
+  return { total, pending };
+});
+
+// beforeSave 钩子:订单自动设置 isDeleted 默认值
+Parse.Cloud.beforeSave('Order', (req) => {
+  if (req.object.isNew() && req.object.get('isDeleted') === undefined) {
+    req.object.set('isDeleted', false);
+  }
+});
+
+// afterSave 钩子:日志记录
+Parse.Cloud.afterSave('Order', (req) => {
+  console.log(`Order saved: ${req.object.id}`);
+});

+ 201 - 0
scripts/seed.ts

@@ -0,0 +1,201 @@
+// 种子数据脚本 — 将 Mock 数据写入 Parse Server
+import Parse from 'parse/node';
+
+Parse.initialize('prd-test');
+Parse.serverURL = 'http://localhost:3000/parse';
+Parse.masterKey = 'prd-test-master-key-2026';
+
+const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
+
+async function seed() {
+  console.log('=== 开始种入数据 ===\n');
+
+  // ===== 1. 用户 =====
+  const users = [
+    { id: 'u001', name: '张明华', role: 'sales', department: '销售一部', phone: '138-0000-1234', email: 'zhang@furnishing.com', password: '1234' },
+    { id: 'u002', name: '李晓燕', role: 'sales', department: '销售一部', phone: '139-0001-2345', email: 'li@furnishing.com', password: '2345' },
+    { id: 'u003', name: '王建国', role: 'reviewer', department: '审单部', phone: '136-0002-3456', email: 'wang@furnishing.com', password: '3456' },
+    { id: 'u004', name: '陈美丽', role: 'finance', department: '财务部', phone: '135-0003-4567', email: 'chen@furnishing.com', password: '4567' },
+    { id: 'u005', name: '刘强', role: 'manager', department: '管理层', phone: '137-0004-5678', email: 'liu@furnishing.com', password: '5678' },
+    { id: 'u006', name: '赵芳', role: 'sales', department: '销售二部', phone: '132-0005-6789', email: 'zhao@furnishing.com', password: '6789' },
+    { id: 'u007', name: '系统管理员', role: 'admin', department: '系统管理', phone: '138-0000-0000', email: 'admin@furnishing.com', password: 'admin123' },
+  ];
+
+  const userIdMap: Record<string, string> = {};
+  for (const u of users) {
+    try {
+      const user = new Parse.User();
+      user.set('username', u.id);
+      user.set('password', u.password);
+      user.set('name', u.name);
+      user.set('role', u.role);
+      user.set('department', u.department);
+      user.set('phone', u.phone);
+      user.set('email', u.email);
+      const saved = await user.signUp(null);
+      userIdMap[u.id] = saved.id;
+      console.log(`  User: ${u.name} (${u.id})`);
+    } catch (e: any) {
+      if (e.message?.includes('already')) {
+        console.log(`  User skip: ${u.name} (already exists)`);
+      } else {
+        console.log(`  User FAIL: ${u.name} — ${e.message}`);
+      }
+    }
+    await sleep(200);
+  }
+
+  // ===== 2. 门店 =====
+  const stores = [
+    '真北北馆一队:ZB1', '真北北馆二队:ZB2', '真北南馆一队:ZN1', '真北南馆二队:ZN2',
+    '浦东一队:PD1', '浦东二队:PD2', '徐汇一队:XH1', '徐汇二队:XH2',
+    '闵行一队:MH1', '闵行二队:MH2', '杨浦一队:YP1', '杨浦二队:YP2',
+    '长宁一队:CN1', '长宁二队:CN2', '虹口一队:HK1', '虹口二队:HK2',
+    '松江一队:SJ1', '松江二队:SJ2', '家装渠道部:JZ1',
+  ];
+  for (const s of stores) {
+    const [name, code] = s.split(':');
+    try {
+      const SC = Parse.Object.extend('StoreConfig');
+      const obj = new SC();
+      obj.set('name', name);
+      obj.set('code', code);
+      await obj.save(null, { useMasterKey: true });
+    } catch (e: any) { console.log(`  Store FAIL: ${name} — ${e.message}`); }
+    await sleep(100);
+  }
+  console.log('  Stores: 19 done');
+
+  // ===== 3. 产品 =====
+  const products = [
+    { name: '定制遮光窗帘', category: '窗帘', subcategory: '遮光帘', baseSpecification: '宽2.5m×高2.8m', unit: '幅', costPrice: 280, salePrice: 580, stock: 48, status: 'active', imageUrl: 'https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?w=400&h=400&fit=crop' },
+    { name: '北欧风棉麻窗帘', category: '窗帘', subcategory: '棉麻帘', baseSpecification: '宽2m×高2.5m', unit: '幅', costPrice: 180, salePrice: 380, stock: 62, status: 'active', imageUrl: 'https://images.unsplash.com/photo-1513694203232-719a280e022f?w=400&h=400&fit=crop' },
+    { name: '羊毛混纺地毯', category: '地毯', subcategory: '客厅地毯', baseSpecification: '2m×3m', unit: '张', costPrice: 680, salePrice: 1480, stock: 15, status: 'active', imageUrl: 'https://images.unsplash.com/photo-1600166898405-da9535204843?w=400&h=400&fit=crop' },
+    { name: '现代简约地毯', category: '地毯', subcategory: '卧室地毯', baseSpecification: '1.5m×2m', unit: '张', costPrice: 320, salePrice: 680, stock: 28, status: 'active', imageUrl: 'https://images.unsplash.com/photo-1555041469-a586c61ea9bc?w=400&h=400&fit=crop' },
+    { name: '意式真皮沙发', category: '沙发', subcategory: '组合沙发', baseSpecification: '3+2+1人座', unit: '套', costPrice: 8800, salePrice: 18800, stock: 6, status: 'active', imageUrl: 'https://images.unsplash.com/photo-1550254478-ead40cc54513?w=400&h=400&fit=crop' },
+    { name: '布艺休闲沙发', category: '沙发', subcategory: '单人沙发', baseSpecification: '单人座 85cm', unit: '把', costPrice: 1200, salePrice: 2600, stock: 12, status: 'active', imageUrl: 'https://images.unsplash.com/photo-1540574163026-643ea20ade25?w=400&h=400&fit=crop' },
+    { name: '四件套床品-云感棉', category: '床品', subcategory: '床上用品', baseSpecification: '1.8m床', unit: '套', costPrice: 380, salePrice: 880, stock: 34, status: 'active', imageUrl: 'https://images.unsplash.com/photo-1522771739844-6a9f6d5f14af?w=400&h=400&fit=crop' },
+    { name: '法式轻奢台灯', category: '灯具', subcategory: '台灯', baseSpecification: '高45cm', unit: '盏', costPrice: 420, salePrice: 980, stock: 20, status: 'active', imageUrl: 'https://images.unsplash.com/photo-1507473885765-e6ed057f782c?w=400&h=400&fit=crop' },
+    { name: '欧式花纹壁纸', category: '墙纸', subcategory: '卧室壁纸', baseSpecification: '0.53m×10m/卷', unit: '卷', costPrice: 180, salePrice: 380, stock: 85, status: 'active' },
+    { name: '抽象装饰画三联', category: '装饰画', subcategory: '挂画', baseSpecification: '60cm×80cm×3幅', unit: '套', costPrice: 280, salePrice: 680, stock: 18, status: 'active' },
+    { name: '铝合金窗帘轨道', category: '窗帘配件', subcategory: '轨道', baseSpecification: '3m/根', unit: '根', costPrice: 45, salePrice: 120, stock: 120, status: 'active' },
+    { name: '抱枕套装', category: '配饰', subcategory: '抱枕', baseSpecification: '45cm×45cm 4个', unit: '套', costPrice: 120, salePrice: 280, stock: 45, status: 'active' },
+  ];
+  for (const p of products) {
+    try {
+      const Product = Parse.Object.extend('Product');
+      const obj = new Product();
+      obj.set('name', p.name);
+      obj.set('category', p.category);
+      obj.set('subcategory', p.subcategory);
+      obj.set('baseSpecification', p.baseSpecification);
+      obj.set('unit', p.unit);
+      obj.set('costPrice', p.costPrice);
+      obj.set('salePrice', p.salePrice);
+      obj.set('stock', p.stock);
+      obj.set('status', p.status);
+      if (p.imageUrl) obj.set('imageUrl', p.imageUrl);
+      await obj.save(null, { useMasterKey: true });
+    } catch (e: any) { console.log(`  Product FAIL: ${p.name} — ${e.message}`); }
+    await sleep(100);
+  }
+  console.log('  Products: 12 done');
+
+  // ===== 4. 订单 =====
+  const orders = [
+    { orderNo: 'SO2024120001', customerName: '万科·天空之城样板间', customerPhone: '021-88880001', customerAddress: '上海市浦东新区张江路100号', customerType: 'commercial', projectName: '万科天空之城A栋样板间', items: [{ productId: 'p001', productName: '定制遮光窗帘', category: '窗帘', specification: '宽2.5m×高2.8m', quantity: 12, unit: '幅', unitPrice: 580, totalPrice: 6960 }], status: 'pending', totalAmount: 12840, paidAmount: 6420, paymentMethod: '分期付款', isInstallment: true, salesPersonId: 'u001', salesPersonName: '张明华', riskLevel: 'medium', attachments: 3 },
+    { orderNo: 'SO2024120002', customerName: '林女士', customerPhone: '138-1234-5678', customerAddress: '上海市徐汇区衡山路258号', customerType: 'residential', projectName: '私宅软装全案', items: [{ productId: 'p005', productName: '意式真皮沙发', category: '沙发', specification: '3+2+1人座', quantity: 1, unit: '套', unitPrice: 18800, totalPrice: 18800 }], status: 'approved', totalAmount: 22800, paidAmount: 22800, paymentMethod: '全款', salesPersonId: 'u002', salesPersonName: '李晓燕', reviewHistory: [{ id: 'rh001', action: 'approved', reviewerId: 'u003', reviewerName: '王建国', comment: '客户资质良好,全款支付,通过。', timestamp: '2024-11-30T10:15:00' }], riskLevel: 'low', attachments: 5 },
+    { orderNo: 'SO2024120003', customerName: '碧桂园·凤凰城', customerPhone: '0755-88880003', customerAddress: '深圳市南山区碧桂园凤凰城', customerType: 'commercial', projectName: '碧桂园凤凰城B区软装', items: [{ productId: 'p002', productName: '北欧风棉麻窗帘', category: '窗帘', specification: '宽2m×高2.5m', quantity: 48, unit: '幅', unitPrice: 380, totalPrice: 18240 }], status: 'rejected', totalAmount: 34560, paidAmount: 0, paymentMethod: '月结', salesPersonId: 'u006', salesPersonName: '赵芳', reviewHistory: [{ id: 'rh002', action: 'rejected', reviewerId: 'u003', reviewerName: '王建国', comment: '月结客户超过授信额度,需要提供担保材料后重新提交。', timestamp: '2024-11-27T16:30:00' }], riskLevel: 'high', attachments: 2, lastRejectedReason: '月结客户超过授信额度' },
+    { orderNo: 'SO2024120004', customerName: '陈先生', customerPhone: '139-8888-9999', customerAddress: '北京市朝阳区三里屯88号', customerType: 'residential', projectName: '三里屯公寓改造', items: [{ productId: 'p009', productName: '欧式花纹壁纸', category: '墙纸', specification: '0.53m×10m/卷', quantity: 20, unit: '卷', unitPrice: 380, totalPrice: 7600 }], status: 'delivered', totalAmount: 12360, paidAmount: 9270, paymentMethod: '分期付款', isInstallment: true, salesPersonId: 'u001', salesPersonName: '张明华', reviewHistory: [{ id: 'rh003', action: 'approved', reviewerId: 'u003', reviewerName: '王建国', comment: '正常业务,通过。', timestamp: '2024-11-21T09:00:00' }], riskLevel: 'low', attachments: 4 },
+    { orderNo: 'SO2024120005', customerName: '保利·天汇', customerPhone: '028-88880005', customerAddress: '成都市锦江区保利天汇', customerType: 'commercial', projectName: '保利天汇售楼处软装', items: [{ productId: 'p001', productName: '定制遮光窗帘', category: '窗帘', specification: '宽2.5m×高2.8m', quantity: 20, unit: '幅', unitPrice: 580, totalPrice: 11600 }], status: 'pending', totalAmount: 34600, paidAmount: 0, paymentMethod: '到货付款', salesPersonId: 'u002', salesPersonName: '李晓燕', riskLevel: 'high', attachments: 1 },
+    { orderNo: 'SO2024120006', customerName: '王女士', customerPhone: '186-6666-7777', customerAddress: '广州市天河区珠江新城168号', customerType: 'residential', projectName: '珠江新城私宅软装', items: [{ productId: 'p005', productName: '意式真皮沙发', category: '沙发', specification: '3+2+1人座', quantity: 1, unit: '套', unitPrice: 18800, totalPrice: 18800 }], status: 'completed', totalAmount: 23440, paidAmount: 23440, paymentMethod: '全款', salesPersonId: 'u001', salesPersonName: '张明华', reviewHistory: [{ id: 'rh004', action: 'approved', reviewerId: 'u003', reviewerName: '王建国', comment: '通过。', timestamp: '2024-11-16T11:00:00' }], riskLevel: 'low', attachments: 6 },
+    { orderNo: 'SO2024120007', customerName: '龙湖·星悦荟', customerPhone: '023-88880007', customerAddress: '重庆市渝中区龙湖星悦荟', customerType: 'commercial', projectName: '龙湖星悦荟商业空间', items: [{ productId: 'p002', productName: '北欧风棉麻窗帘', category: '窗帘', specification: '宽2m×高2.5m', quantity: 30, unit: '幅', unitPrice: 380, totalPrice: 11400 }], status: 'draft', totalAmount: 16840, paidAmount: 0, paymentMethod: '月结', salesPersonId: 'u001', salesPersonName: '张明华', riskLevel: 'medium', attachments: 0 },
+  ];
+  for (const o of orders) {
+    try {
+      const Order = Parse.Object.extend('Order');
+      const obj = new Order();
+      obj.set('orderNo', o.orderNo);
+      obj.set('customerName', o.customerName);
+      obj.set('customerPhone', o.customerPhone);
+      obj.set('customerAddress', o.customerAddress);
+      obj.set('customerType', o.customerType);
+      obj.set('projectName', o.projectName);
+      obj.set('items', o.items);
+      obj.set('status', o.status);
+      obj.set('totalAmount', o.totalAmount);
+      obj.set('paidAmount', o.paidAmount);
+      obj.set('paymentMethod', o.paymentMethod);
+      obj.set('salesPersonId', o.salesPersonId);
+      obj.set('salesPersonName', o.salesPersonName);
+      obj.set('riskLevel', o.riskLevel);
+      obj.set('attachments', o.attachments);
+      obj.set('isDeleted', false);
+      if (o.isInstallment) obj.set('isInstallment', true);
+      if (o.reviewHistory) obj.set('reviewHistory', o.reviewHistory);
+      if (o.lastRejectedReason) obj.set('lastRejectedReason', o.lastRejectedReason);
+      await obj.save(null, { useMasterKey: true });
+    } catch (e: any) { console.log(`  Order FAIL: ${o.orderNo} — ${e.message}`); }
+    await sleep(100);
+  }
+  console.log('  Orders: 7 done');
+
+  // ===== 5. 财务记录 =====
+  const finances = [
+    { orderNo: 'SO2024120002', customerName: '林女士', orderDate: '2024-11-28', dueDate: '2024-11-28', receivable: 22800, received: 22800, difference: 0, status: 'normal', commission: 1368, salesPerson: '李晓燕', verified: true },
+    { orderNo: 'SO2024120004', customerName: '陈先生', orderDate: '2024-11-20', dueDate: '2024-12-20', receivable: 12360, received: 9270, difference: 3090, status: 'partial', commission: 741.6, salesPerson: '张明华', verified: false },
+    { orderNo: 'SO2024120006', customerName: '王女士', orderDate: '2024-11-15', dueDate: '2024-11-15', receivable: 23440, received: 23440, difference: 0, status: 'normal', commission: 1406.4, salesPerson: '张明华', verified: true },
+    { orderNo: 'SO2024110001', customerName: '融创·一号院', orderDate: '2024-11-01', dueDate: '2024-11-30', receivable: 68000, received: 45000, difference: 23000, status: 'overdue', commission: 4080, salesPerson: '赵芳', verified: false },
+    { orderNo: 'SO2024110002', customerName: '孙先生', orderDate: '2024-11-05', dueDate: '2024-12-05', receivable: 8800, received: 8800, difference: 0, status: 'normal', commission: 528, salesPerson: '李晓燕', verified: true },
+  ];
+  for (const f of finances) {
+    try {
+      const FR = Parse.Object.extend('FinanceRecord');
+      const obj = new FR();
+      obj.set('orderNo', f.orderNo);
+      obj.set('customerName', f.customerName);
+      obj.set('orderDate', f.orderDate);
+      obj.set('dueDate', f.dueDate);
+      obj.set('receivable', f.receivable);
+      obj.set('received', f.received);
+      obj.set('difference', f.difference);
+      obj.set('status', f.status);
+      obj.set('commission', f.commission);
+      obj.set('salesPerson', f.salesPerson);
+      obj.set('verified', f.verified);
+      await obj.save(null, { useMasterKey: true });
+    } catch (e: any) { console.log(`  Finance FAIL: ${f.orderNo} — ${e.message}`); }
+    await sleep(100);
+  }
+  console.log('  Finance: 5 done');
+
+  // ===== 6. 消息 =====
+  const messages = [
+    { type: 'approval', title: '订单待您审核', content: '订单 SO2024120001 已提交,请及时审核。', isRead: false, linkPath: '/approval', relatedId: 'o001', recipientId: 'u003' },
+    { type: 'approval', title: '订单待您审核', content: '订单 SO2024120005 已提交,请及时审核。', isRead: false, linkPath: '/approval', relatedId: 'o005', recipientId: 'u003' },
+    { type: 'finance', title: '账款逾期提醒', content: '融创·一号院订单逾期未收款 ¥23,000,请跟进。', isRead: false, linkPath: '/finance', relatedId: 'f004', recipientId: 'u004' },
+    { type: 'order', title: '订单已审核通过', content: '您的订单 SO2024120002 已通过审核。', isRead: true, linkPath: '/orders', relatedId: 'o002', recipientId: 'u002' },
+    { type: 'order', title: '订单被打回', content: '订单 SO2024120003 被审核打回。', isRead: true, linkPath: '/orders', relatedId: 'o003', recipientId: 'u006' },
+    { type: 'system', title: '系统维护通知', content: '系统将于12月10日升级维护。', isRead: true, linkPath: null, relatedId: null, recipientId: null },
+  ];
+  for (const m of messages) {
+    try {
+      const Msg = Parse.Object.extend('Message');
+      const obj = new Msg();
+      obj.set('type', m.type);
+      obj.set('title', m.title);
+      obj.set('content', m.content);
+      obj.set('isRead', m.isRead);
+      if (m.linkPath) obj.set('linkPath', m.linkPath);
+      if (m.relatedId) obj.set('relatedId', m.relatedId);
+      if (m.recipientId) obj.set('recipientId', m.recipientId);
+      await obj.save(null, { useMasterKey: true });
+    } catch (e: any) { console.log(`  Message FAIL: ${m.title} — ${e.message}`); }
+    await sleep(100);
+  }
+  console.log('  Messages: 6 done');
+
+  console.log('\n=== 种入完成 ===');
+}
+
+seed().catch(e => { console.error(e); process.exit(1); });

+ 99 - 0
server.ts

@@ -0,0 +1,99 @@
+// 拉迷软装一体化管理系统 — 模式 B 服务端入口
+// Express + Parse Server 自建模式
+
+import 'dotenv/config';
+import express from 'express';
+import cors from 'cors';
+import { ParseServer } from 'parse-server';
+import Parse from 'parse/node';
+import { createRequire } from 'module';
+
+// 加载配置
+import configJson from './config.json' with { type: 'json' };
+const parseConfig = configJson.parse;
+
+const require = createRequire(import.meta.url);
+
+async function startServer() {
+  // =============================================
+  // 1. Express 应用
+  // =============================================
+  const app = express();
+  app.use(cors());
+  app.use(express.json({ limit: '10mb' }));
+  app.use(express.urlencoded({ extended: true }));
+
+  // =============================================
+  // 2. Parse Server
+  // =============================================
+  const parseServer = new ParseServer({
+    appId: parseConfig.appId,
+    masterKey: parseConfig.masterKey,
+    databaseURI: parseConfig.databaseURI,
+    serverURL: parseConfig.serverURL,
+    cloud: undefined,  // 手动加载(见下方)
+    allowClientClassCreation: parseConfig.allowClientClassCreation ?? true,
+    enableAnonymousUsers: parseConfig.enableAnonymousUsers ?? false,
+  });
+
+  await parseServer.start();
+  app.use('/parse', parseServer.app);
+  console.log(`[Parse] Server started at ${parseConfig.serverURL}`);
+
+  // =============================================
+  // 3. Parse SDK 客户端(注入全局,供 api/module 使用)
+  // =============================================
+  Parse.initialize(parseConfig.appId);
+  Parse.serverURL = parseConfig.serverURL;
+  Parse.masterKey = parseConfig.masterKey;
+  (globalThis as any).Parse = Parse;
+  console.log(`[Parse] SDK initialized (appId: ${parseConfig.appId})`);
+
+  // =============================================
+  // 4. Cloud Code(手动加载)
+  // =============================================
+  if (parseConfig.cloud) {
+    try {
+      require(parseConfig.cloud);
+      console.log(`[Cloud] Loaded: ${parseConfig.cloud}`);
+    } catch (e: any) {
+      console.warn(`[Cloud] Failed to load ${parseConfig.cloud}:`, e.message);
+    }
+  }
+
+  // =============================================
+  // 5. 自定义 API 路由
+  // =============================================
+  try {
+    const apiRoutes = await import('./api/routes.js');
+    app.use('/api', apiRoutes.default);
+    console.log('[API] Custom routes mounted at /api');
+  } catch (e: any) {
+    console.warn('[API] No api/routes.ts found, skipping custom routes:', e.message);
+  }
+
+  // =============================================
+  // 6. 健康检查
+  // =============================================
+  app.get('/api/health', (_, res) => {
+    res.json({ ok: true, timestamp: new Date().toISOString(), appId: parseConfig.appId });
+  });
+
+  // =============================================
+  // 7. 启动
+  // =============================================
+  const PORT = process.env.PORT || parseConfig.port || 3000;
+  app.listen(PORT, () => {
+    console.log(`\n========================================`);
+    console.log(`  拉迷软装一体化管理系统 API`);
+    console.log(`  http://localhost:${PORT}`);
+    console.log(`  Parse: http://localhost:${PORT}/parse`);
+    console.log(`  Health: http://localhost:${PORT}/api/health`);
+    console.log(`========================================\n`);
+  });
+}
+
+startServer().catch((err) => {
+  console.error('Failed to start server:', err);
+  process.exit(1);
+});