PSI移动端小程序对接指南
移动端应用场景
随着移动办公需求的增长,PSI 进销存软件需要支持手机端操作。常见的移动端应用场景包括:
- 扫码出入库:使用手机扫描商品条码快速完成出入库操作
- 库存查询:随时查看各仓库商品库存情况
- 销售开单:使用手机快速创建销售订单
- 审批流程:移动端审批采购、销售等业务流程
- 数据看板:实时查看经营数据和销售报表
整体架构设计
┌─────────────────────────────────────────────────────────┐
│ 微信小程序 / App │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │扫码出入库│ │库存查询 │ │销售开单 │ │数据看板 │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │
└───────┼────────────┼────────────┼────────────┼──────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────┐
│ API 网关 (Nginx) │
│ 鉴权、限流、路由、SSL 加速 │
└───────────────────────────┬─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ PSI 后端服务 (Node.js/Koa) │
│ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌──────────┐ │
│ │ 用户服务 │ │ 商品服务 │ │ 库存服务 │ │ 订单服务 │ │
│ └────┬────┘ └────┬─────┘ └────┬────┘ └────┬─────┘ │
└───────┼────────────┼────────────┼────────────┼─────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────┐
│ MySQL 数据库集群 │
└─────────────────────────────────────────────────────────┘
小程序对接实现
1. 小程序主要页面结构
| 页面 | 路径 | 功能说明 |
|---|---|---|
| 首页 | pages/index/index | 快捷功能、待办事项、数据概览 |
| 扫码 | pages/scan/scan | 扫码识别商品、快速出入库 |
| 库存 | pages/inventory/inventory | 库存查询、库存预警 |
| 开单 | pages/order/create | 创建采购/销售订单 |
| 我的 | pages/profile/profile | 个人信息、设置、退出 |
2. 小程序请求封装
// utils/request.js - 请求封装
import { getToken } from '../utils/auth';
const BASE_URL = 'https://api.psi.js.cn';
class Request {
request(url, options = {}) {
const token = getToken();
return new Promise((resolve, reject) => {
wx.request({
url: BASE_URL + url,
...options,
header: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
...options.header
},
success: (res) => {
if (res.statusCode === 200) {
if (res.data.code === 0) {
resolve(res.data.data);
} else if (res.data.code === 401) {
// token 过期,重新登录
this.handleUnauthorized();
reject(res.data);
} else {
wx.showToast({
title: res.data.message || '请求失败',
icon: 'none'
});
reject(res.data);
}
} else {
reject({ message: '服务器错误' });
}
},
fail: (err) => {
wx.showToast({
title: '网络请求失败',
icon: 'none'
});
reject(err);
}
});
});
}
get(url, params, options) {
const queryString = new URLSearchParams(params).toString();
return this.request(url + (queryString ? `?${queryString}` : ''), {
...options,
method: 'GET'
});
}
post(url, data, options) {
return this.request(url, {
...options,
method: 'POST',
data
});
}
handleUnauthorized() {
wx.removeStorageSync('token');
wx.reLaunch({ url: '/pages/login/login' });
}
}
export default new Request();
3. 扫码出入库实现
// pages/scan/scan.js
import Request from '../../utils/request';
import { scanProduct } from '../../api/product';
Page({
data: {
scanResult: null,
warehouseList: [],
warehouseId: '',
quantity: 1,
type: 'in' // 'in' 入库, 'out' 出库
},
onLoad() {
this.loadWarehouses();
},
// 扫描条码
handleScan() {
wx.scanCode({
success: (res) => {
this.queryProduct(res.result);
},
fail: (err) => {
wx.showToast({ title: '扫描失败', icon: 'none' });
}
});
},
// 根据条码查询商品
async queryProduct(barcode) {
wx.showLoading({ title: '查询中...' });
try {
const product = await Request.get('/api/product/barcode', { barcode });
this.setData({
scanResult: product,
quantity: 1
});
} catch (err) {
wx.showToast({
title: '商品未找到',
icon: 'none'
});
} finally {
wx.hideLoading();
}
},
// 提交出入库
async handleSubmit() {
const { scanResult, warehouseId, quantity, type } = this.data;
if (!scanResult) {
return wx.showToast({ title: '请先扫描商品', icon: 'none' });
}
if (!warehouseId) {
return wx.showToast({ title: '请选择仓库', icon: 'none' });
}
wx.showLoading({ title: '提交中...' });
try {
await Request.post('/api/inventory/record', {
productId: scanResult.id,
warehouseId,
quantity,
type,
operator: getApp().globalData.userInfo.name
});
wx.showToast({
title: type === 'in' ? '入库成功' : '出库成功',
icon: 'success'
});
// 重置表单
this.setData({
scanResult: null,
quantity: 1
});
} catch (err) {
wx.showToast({
title: err.message || '操作失败',
icon: 'none'
});
} finally {
wx.hideLoading();
}
},
// 加载仓库列表
async loadWarehouses() {
try {
const list = await Request.get('/api/warehouse/list');
this.setData({
warehouseList: list,
warehouseId: list[0]?.id || ''
});
} catch (err) {
console.error('加载仓库失败', err);
}
}
});
4. 库存查询页面
// pages/inventory/inventory.js
import Request from '../../utils/request';
Page({
data: {
inventoryList: [],
searchKeyword: '',
loading: false,
page: 1,
pageSize: 20,
hasMore: true
},
onLoad() {
this.loadInventory();
},
// 下拉刷新
async onPullDownRefresh() {
this.setData({ page: 1, inventoryList: [], hasMore: true });
await this.loadInventory();
wx.stopPullDownRefresh();
},
// 上拉加载更多
async onReachBottom() {
if (!this.data.hasMore) return;
this.setData({ page: this.data.page + 1 });
await this.loadInventory();
},
// 搜索
onSearch(e) {
this.setData({
searchKeyword: e.detail.value,
page: 1,
inventoryList: [],
hasMore: true
});
this.loadInventory();
},
async loadInventory() {
const { page, pageSize, searchKeyword } = this.data;
this.setData({ loading: true });
try {
const list = await Request.get('/api/inventory/list', {
page,
pageSize,
keyword: searchKeyword
});
this.setData({
inventoryList: page === 1
? list
: [...this.data.inventoryList, ...list],
hasMore: list.length >= pageSize
});
} catch (err) {
wx.showToast({ title: '加载失败', icon: 'none' });
} finally {
this.setData({ loading: false });
}
},
// 查看商品详情
goDetail(e) {
const id = e.currentTarget.dataset.id;
wx.navigateTo({
url: `/pages/inventory/detail?id=${id}`
});
}
});
后端 API 接口
| 接口 | 说明 |
|---|---|
| GET /api/product/barcode | 根据条码查询商品 |
| POST /api/inventory/record | 记录出入库 |
| GET /api/inventory/list | 库存列表查询 |
| GET /api/warehouse/list | 仓库列表 |
总结
通过小程序对接,PSI 可以实现移动办公的需求。扫码出入库功能特别适合仓库操作人员使用,可以大幅提高工作效率。后端采用 RESTful API 设计,支持未来扩展更多移动端功能。