feat: 添加手机号登录和短信验证码功能,更新 API 地址,重构用户状态管理
This commit is contained in:
696
API_GUIDE.md
Normal file
696
API_GUIDE.md
Normal file
@@ -0,0 +1,696 @@
|
||||
# 云印享 - 前端 API 使用指南
|
||||
|
||||
本文档面向前端开发者,详细说明如何调用云印享后端 API 接口。后端基于 gRPC 构建,前端通过 HTTP RESTful 风格接口进行通信。
|
||||
|
||||
## 目录
|
||||
|
||||
- [基础配置](#基础配置)
|
||||
- [认证服务 (Auth)](#认证服务-auth)
|
||||
- [文件管理服务 (File)](#文件管理服务-file)
|
||||
- [订单服务 (Order)](#订单服务-order)
|
||||
- [打印服务 (Print)](#打印服务-print)
|
||||
- [支付服务 (Pay)](#支付服务-pay)
|
||||
- [价目表服务 (Price)](#价目表服务-price)
|
||||
- [错误处理](#错误处理)
|
||||
- [注意事项](#注意事项)
|
||||
|
||||
---
|
||||
|
||||
## 基础配置
|
||||
|
||||
### API 基础地址
|
||||
|
||||
API 地址配置在 [`src/config/api.ts`](src/config/api.ts:1) 中:
|
||||
|
||||
```typescript
|
||||
const DEV_API_URL = "http://localhost:8080";
|
||||
const PROD_API_URL = "http://localhost:8080";
|
||||
export const API_BASE_URL =
|
||||
process.env.NODE_ENV === "development" ? DEV_API_URL : PROD_API_URL;
|
||||
```
|
||||
|
||||
### 请求封装
|
||||
|
||||
项目已封装统一的请求工具 [`src/utils/request.ts`](src/utils/request.ts:1),自动处理:
|
||||
|
||||
- Token 注入(Bearer 认证)
|
||||
- 401 未授权处理(自动登出)
|
||||
- 错误捕获
|
||||
|
||||
```typescript
|
||||
// 使用示例
|
||||
import { request } from "../utils/request";
|
||||
|
||||
const response = await request({
|
||||
url: "/auth/userinfo",
|
||||
method: "GET",
|
||||
});
|
||||
```
|
||||
|
||||
### 统一响应格式
|
||||
|
||||
所有 API 返回遵循统一格式:
|
||||
|
||||
```typescript
|
||||
interface ApiResponse<T = any> {
|
||||
code: number; // 状态码,0 表示成功
|
||||
message: string; // 响应消息
|
||||
data: T; // 实际数据
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 认证服务 (Auth)
|
||||
|
||||
**基础路径**: `/auth`
|
||||
|
||||
### 1. 微信登录
|
||||
|
||||
获取用户访问令牌。
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | ------------------------------------------------------------------- |
|
||||
| **接口地址** | `POST /auth/login` |
|
||||
| **请求参数** | `{ code: string }` - 微信登录凭证 code |
|
||||
| **返回数据** | `{ accessToken: string; refreshToken: string; userInfo: UserInfo }` |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
import { authApi } from "./api/auth";
|
||||
|
||||
// 调用登录接口
|
||||
const response = await authApi.login(wxCode);
|
||||
// response.accessToken // 访问令牌
|
||||
// response.refreshToken // 刷新令牌
|
||||
// response.userInfo // 用户信息
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
|
||||
```json
|
||||
{
|
||||
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"refreshToken": "dGhpcyBpcyBhIHJlZnJl...",
|
||||
"userInfo": {
|
||||
"id": 1,
|
||||
"openid": "oXXXX-xxxxx",
|
||||
"nickname": "用户昵称",
|
||||
"avatar": "https://xxx.com/avatar.jpg",
|
||||
"balance": 10.5,
|
||||
"createdAt": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 刷新 Token
|
||||
|
||||
使用刷新令牌获取新的访问令牌。
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | -------------------------- |
|
||||
| **接口地址** | `POST /auth/refresh` |
|
||||
| **请求参数** | `{ refreshToken: string }` |
|
||||
| **返回数据** | `{ accessToken: string }` |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
const { accessToken } = await authApi.refreshToken(refreshToken);
|
||||
```
|
||||
|
||||
### 3. 获取用户信息
|
||||
|
||||
获取当前登录用户的详细信息。
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | --------------------------------- |
|
||||
| **接口地址** | `GET /auth/userinfo` |
|
||||
| **请求参数** | 无 |
|
||||
| **返回数据** | [`UserInfo`](src/types/user.ts:1) |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
const userInfo = await authApi.getUserInfo();
|
||||
console.log(userInfo.nickname, userInfo.balance);
|
||||
```
|
||||
|
||||
### 4. 更新用户信息
|
||||
|
||||
更新用户昵称和头像。
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | ---------------------------------------- |
|
||||
| **接口地址** | `POST /auth/update` |
|
||||
| **请求参数** | `{ nickname?: string; avatar?: string }` |
|
||||
| **返回数据** | [`UserInfo`](src/types/user.ts:1) |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
const updatedUser = await authApi.updateUser({
|
||||
nickname: "新昵称",
|
||||
avatar: "https://new-avatar.jpg",
|
||||
});
|
||||
```
|
||||
|
||||
### 5. 手机号登录
|
||||
|
||||
使用手机号和短信验证码登录。
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | ------------------------------------------------------------------- |
|
||||
| **接口地址** | `POST /auth/phone/login` |
|
||||
| **请求参数** | `{ phone: string; code: string }` |
|
||||
| **返回数据** | `{ accessToken: string; refreshToken: string; userInfo: UserInfo }` |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
import { authApi } from "./api/auth";
|
||||
|
||||
// 调用手机号登录接口
|
||||
const response = await authApi.phoneLogin({
|
||||
phone: "13800138000",
|
||||
code: "123456",
|
||||
});
|
||||
```
|
||||
|
||||
### 6. 发送短信验证码
|
||||
|
||||
发送短信验证码到指定手机号。
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | --------------------- |
|
||||
| **接口地址** | `POST /auth/sms/code` |
|
||||
| **请求参数** | `{ phone: string }` |
|
||||
| **返回数据** | `void` |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
import { authApi } from "./api/auth";
|
||||
|
||||
// 发送短信验证码
|
||||
await authApi.sendSmsCode({ phone: "13800138000" });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 文件管理服务 (File)
|
||||
|
||||
**基础路径**: `/file`
|
||||
|
||||
### 1. 上传文件
|
||||
|
||||
支持分片上传,使用 Taro 原生上传能力。
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | ------------------------------------- |
|
||||
| **接口地址** | `POST /file/upload` |
|
||||
| **请求参数** | `filePath: string` - 本地文件路径 |
|
||||
| **返回数据** | `{ fileId: number; fileUrl: string }` |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
import { fileApi } from "./api/file";
|
||||
|
||||
// 上传文件
|
||||
const result = await fileApi.upload(tempFilePath);
|
||||
console.log(result.fileId, result.fileUrl);
|
||||
```
|
||||
|
||||
### 2. 获取文件列表
|
||||
|
||||
分页获取用户上传的文件列表。
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | ----------------------------------------------------------------- |
|
||||
| **接口地址** | `GET /file/list` |
|
||||
| **请求参数** | `{ page?: number; pageSize?: number }` - 默认 page=1, pageSize=20 |
|
||||
| **返回数据** | `{ files: FileInfo[]; total: number }` |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
const { files, total } = await fileApi.getFileList(1, 20);
|
||||
```
|
||||
|
||||
### 3. 获取文件详情
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | --------------------------------- |
|
||||
| **接口地址** | `GET /file/:fileId` |
|
||||
| **请求参数** | `fileId: number` |
|
||||
| **返回数据** | [`FileInfo`](src/types/file.ts:1) |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
const fileInfo = await fileApi.getFile(123);
|
||||
```
|
||||
|
||||
### 4. 删除文件
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | ---------------------- |
|
||||
| **接口地址** | `DELETE /file/:fileId` |
|
||||
| **请求参数** | `fileId: number` |
|
||||
| **返回数据** | `void` |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
await fileApi.deleteFile(123);
|
||||
```
|
||||
|
||||
### 5. 添加水印
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | ----------------------------------------- |
|
||||
| **接口地址** | `POST /file/:fileId/watermark` |
|
||||
| **请求参数** | `fileId: number`, `{ watermark: string }` |
|
||||
| **返回数据** | `{ watermarkUrl: string }` |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
const { watermarkUrl } = await fileApi.addWatermark(123, "仅供打印使用");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 订单服务 (Order)
|
||||
|
||||
**基础路径**: `/order`
|
||||
|
||||
### 1. 创建订单
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | --------------------------------------------- |
|
||||
| **接口地址** | `POST /order/create` |
|
||||
| **请求参数** | [`CreateOrderRequest`](src/types/order.ts:22) |
|
||||
| **返回数据** | [`Order`](src/types/order.ts:4) |
|
||||
|
||||
**请求参数结构**:
|
||||
|
||||
```typescript
|
||||
{
|
||||
fileId: number; // 文件ID
|
||||
printerId: number; // 打印机ID
|
||||
printType: "A4" | "A3" | "照片"; // 打印类型
|
||||
colorType: "黑白" | "彩色"; // 颜色类型
|
||||
copies: number; // 份数
|
||||
}
|
||||
```
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
import { orderApi } from "./api/order";
|
||||
|
||||
const order = await orderApi.createOrder({
|
||||
fileId: 123,
|
||||
printerId: 1,
|
||||
printType: "A4",
|
||||
colorType: "黑白",
|
||||
copies: 2,
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 获取订单详情
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | ------------------------------- |
|
||||
| **接口地址** | `GET /order/:orderId` |
|
||||
| **请求参数** | `orderId: number` |
|
||||
| **返回数据** | [`Order`](src/types/order.ts:4) |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
const order = await orderApi.getOrder(456);
|
||||
```
|
||||
|
||||
### 3. 获取订单列表
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | -------------------------------------- |
|
||||
| **接口地址** | `GET /order/list` |
|
||||
| **请求参数** | `{ page?: number; pageSize?: number }` |
|
||||
| **返回数据** | `{ orders: Order[]; total: number }` |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
const { orders, total } = await orderApi.getOrderList(1, 10);
|
||||
```
|
||||
|
||||
### 4. 更新订单状态
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | --------------------------------------- |
|
||||
| **接口地址** | `PUT /order/:orderId/status` |
|
||||
| **请求参数** | `orderId: number`, `{ status: number }` |
|
||||
| **返回数据** | [`Order`](src/types/order.ts:4) |
|
||||
|
||||
**订单状态枚举**:
|
||||
| 值 | 状态 |
|
||||
|----|------|
|
||||
| 0 | 待支付 |
|
||||
| 1 | 已支付 |
|
||||
| 2 | 打印中 |
|
||||
| 3 | 已完成 |
|
||||
| 4 | 已取消 |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
await orderApi.updateOrderStatus(456, 1); // 标记为已支付
|
||||
```
|
||||
|
||||
### 5. 取消订单
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | ------------------------------- |
|
||||
| **接口地址** | `PUT /order/:orderId/cancel` |
|
||||
| **请求参数** | `orderId: number` |
|
||||
| **返回数据** | [`Order`](src/types/order.ts:4) |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
await orderApi.cancelOrder(456);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 打印服务 (Print)
|
||||
|
||||
**基础路径**: `/print`
|
||||
|
||||
### 1. 获取打印机列表
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | ------------------------------------- |
|
||||
| **接口地址** | `GET /print/printers` |
|
||||
| **请求参数** | 无 |
|
||||
| **返回数据** | [`Printer[]`](src/types/printer.ts:3) |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
import { printerApi } from "./api/printer";
|
||||
|
||||
const printers = await printerApi.getPrinters();
|
||||
```
|
||||
|
||||
### 2. 获取打印机状态
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | -------------------------------------- |
|
||||
| **接口地址** | `GET /print/printer/:printerId/status` |
|
||||
| **请求参数** | `printerId: number` |
|
||||
| **返回数据** | [`Printer`](src/types/printer.ts:3) |
|
||||
|
||||
**打印机状态枚举**:
|
||||
| 值 | 状态 |
|
||||
|----|------|
|
||||
| 0 | 空闲 |
|
||||
| 1 | 打印中 |
|
||||
| 2 | 离线 |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
const printer = await printerApi.getPrinterStatus(1);
|
||||
```
|
||||
|
||||
### 3. 注册打印机
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | ------------------------------------ |
|
||||
| **接口地址** | `POST /print/printer/register` |
|
||||
| **请求参数** | `{ name: string; location: string }` |
|
||||
| **返回数据** | [`Printer`](src/types/printer.ts:3) |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
const printer = await printerApi.registerPrinter("打印机1", "一楼大厅");
|
||||
```
|
||||
|
||||
### 4. 提交打印任务
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | ---------------------------------------- |
|
||||
| **接口地址** | `POST /print/job/submit` |
|
||||
| **请求参数** | `{ orderId: number; printerId: number }` |
|
||||
| **返回数据** | `{ jobId: number }` |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
const { jobId } = await printerApi.submitPrintJob(456, 1);
|
||||
```
|
||||
|
||||
### 5. 取消打印任务
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | ------------------------------ |
|
||||
| **接口地址** | `PUT /print/job/:jobId/cancel` |
|
||||
| **请求参数** | `jobId: number` |
|
||||
| **返回数据** | `void` |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
await printerApi.cancelPrintJob(789);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 支付服务 (Pay)
|
||||
|
||||
**基础路径**: `/pay`
|
||||
|
||||
### 1. 创建微信支付订单
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | ---------------------------------------------------------------------------------------------- |
|
||||
| **接口地址** | `POST /pay/create` |
|
||||
| **请求参数** | `{ orderId: number }` |
|
||||
| **返回数据** | `{ prepayId: string; paySign: string; timeStamp: string; nonceStr: string; signType: string }` |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
import { payApi } from "./api/pay";
|
||||
|
||||
const payParams = await payApi.createWxPayOrder(456);
|
||||
// 使用返回参数调起微信支付
|
||||
```
|
||||
|
||||
### 2. 查询支付状态
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | ------------------------- |
|
||||
| **接口地址** | `GET /pay/query/:orderId` |
|
||||
| **请求参数** | `orderId: number` |
|
||||
| **返回数据** | `{ status: number }` |
|
||||
|
||||
**支付状态枚举**:
|
||||
| 值 | 状态 |
|
||||
|----|------|
|
||||
| 0 | 待支付 |
|
||||
| 1 | 支付中 |
|
||||
| 2 | 成功 |
|
||||
| 3 | 失败 |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
const { status } = await payApi.queryPayStatus(456);
|
||||
```
|
||||
|
||||
### 3. 余额支付
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | ---------------------- |
|
||||
| **接口地址** | `POST /pay/balance` |
|
||||
| **请求参数** | `{ orderId: number }` |
|
||||
| **返回数据** | `{ success: boolean }` |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
const { success } = await payApi.payByBalance(456);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 价目表服务 (Price)
|
||||
|
||||
**基础路径**: `/price`
|
||||
|
||||
### 1. 获取价目表
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ------------ | ------------------------------------- |
|
||||
| **接口地址** | `GET /price/list` |
|
||||
| **请求参数** | 无 |
|
||||
| **返回数据** | [`PriceItem[]`](src/types/price.ts:3) |
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```typescript
|
||||
import { priceApi } from "./api/price";
|
||||
|
||||
const priceList = await priceApi.getPriceList();
|
||||
```
|
||||
|
||||
**价目表示例**:
|
||||
|
||||
```json
|
||||
[
|
||||
{ "id": 1, "name": "A4黑白打印", "type": "A4", "price": 0.1, "unit": "页" },
|
||||
{ "id": 2, "name": "A4彩色打印", "type": "A4", "price": 0.3, "unit": "页" },
|
||||
{ "id": 3, "name": "A3黑白打印", "type": "A3", "price": 0.2, "unit": "页" },
|
||||
{ "id": 4, "name": "A3彩色打印", "type": "A3", "price": 0.6, "unit": "页" },
|
||||
{ "id": 5, "name": "照片打印", "type": "照片", "price": 2.0, "unit": "张" }
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
### HTTP 状态码
|
||||
|
||||
| 状态码 | 说明 | 处理建议 |
|
||||
| ------ | -------------- | ---------------------------- |
|
||||
| 200 | 请求成功 | 正常处理响应数据 |
|
||||
| 400 | 请求参数错误 | 检查请求参数格式 |
|
||||
| 401 | 未授权 | Token 过期或无效,需重新登录 |
|
||||
| 403 | 禁止访问 | 权限不足 |
|
||||
| 404 | 资源不存在 | 检查请求的资源 ID |
|
||||
| 500 | 服务器内部错误 | 联系后端排查 |
|
||||
|
||||
### 统一错误处理
|
||||
|
||||
[`request.ts`](src/utils/request.ts:1) 已封装错误处理逻辑:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const result = await orderApi.createOrder(orderData);
|
||||
} catch (error) {
|
||||
if (error.message === "Unauthorized") {
|
||||
// 处理未授权,跳转登录
|
||||
Taro.redirectTo({ url: "/pages/login/index" });
|
||||
} else {
|
||||
// 显示错误提示
|
||||
Taro.showToast({ title: error.message, icon: "none" });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. 认证机制
|
||||
|
||||
- 所有需要用户身份的接口都需要在请求头中携带 `Authorization: Bearer <token>`
|
||||
- Token 由 [`request.ts`](src/utils/request.ts:16) 自动注入,无需手动处理
|
||||
- Token 过期时,服务器返回 401,前端自动触发登出
|
||||
|
||||
### 2. 文件上传
|
||||
|
||||
- 使用 Taro 原生 `uploadFile` API,支持断点续传
|
||||
- 上传成功后返回 `fileId` 和 `fileUrl`,用于后续订单创建
|
||||
|
||||
### 3. 分页查询
|
||||
|
||||
- 文件列表和订单列表接口支持分页
|
||||
- 默认 `page=1`, `pageSize=20`
|
||||
- 响应中包含 `total` 字段表示总记录数
|
||||
|
||||
### 4. 状态枚举
|
||||
|
||||
- 订单状态、支付状态、打印机状态均使用数字枚举
|
||||
- 类型定义文件(如 [`order.ts`](src/types/order.ts:1))中提供了状态映射表,便于显示
|
||||
|
||||
### 5. 数据格式
|
||||
|
||||
- 所有时间字段使用 ISO 8601 格式字符串(如 `"2024-01-01T12:00:00Z"`)
|
||||
- 价格字段使用数字类型,单位为元(如 `0.10` 表示 0.1 元)
|
||||
|
||||
### 6. 开发环境配置
|
||||
|
||||
- 开发环境 API 地址:`http://localhost:8080`
|
||||
- 确保后端服务已启动且端口正确
|
||||
- 如需修改地址,编辑 [`src/config/api.ts`](src/config/api.ts:1)
|
||||
|
||||
### 7. 类型安全
|
||||
|
||||
- 所有 API 接口都有完整的 TypeScript 类型定义
|
||||
- 导入对应的类型文件(如 `import type { Order } from '../types'`)
|
||||
- 使用类型提示可以减少运行时错误
|
||||
|
||||
---
|
||||
|
||||
## 快速开始示例
|
||||
|
||||
以下是一个完整的打印流程示例:
|
||||
|
||||
```typescript
|
||||
import { authApi } from "./api/auth";
|
||||
import { fileApi } from "./api/file";
|
||||
import { orderApi } from "./api/order";
|
||||
import { printerApi } from "./api/printer";
|
||||
import { payApi } from "./api/pay";
|
||||
|
||||
// 1. 登录
|
||||
const { accessToken, userInfo } = await authApi.login(wxCode);
|
||||
|
||||
// 2. 上传文件
|
||||
const { fileId } = await fileApi.upload(tempFilePath);
|
||||
|
||||
// 3. 获取打印机列表
|
||||
const printers = await printerApi.getPrinters();
|
||||
const availablePrinter = printers.find((p) => p.status === 0);
|
||||
|
||||
// 4. 创建订单
|
||||
const order = await orderApi.createOrder({
|
||||
fileId,
|
||||
printerId: availablePrinter.id,
|
||||
printType: "A4",
|
||||
colorType: "黑白",
|
||||
copies: 2,
|
||||
});
|
||||
|
||||
// 5. 发起支付
|
||||
const payParams = await payApi.createWxPayOrder(order.id);
|
||||
// 调起微信支付...
|
||||
|
||||
// 6. 查询支付状态
|
||||
const { status } = await payApi.queryPayStatus(order.id);
|
||||
if (status === 2) {
|
||||
// 支付成功,提交打印任务
|
||||
await printerApi.submitPrintJob(order.id, availablePrinter.id);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 更新日志
|
||||
|
||||
- 2024-01-01: 初始版本,基于后端 README 和前端现有实现整理
|
||||
|
||||
---
|
||||
|
||||
**文档维护**: 前端开发团队
|
||||
**最后更新**: 2024-01-01
|
||||
@@ -1,7 +1,17 @@
|
||||
import { request } from '../utils/request';
|
||||
import type { UserInfo, LoginResponse, UpdateUserRequest } from '../types';
|
||||
|
||||
export interface PhoneLoginRequest {
|
||||
phone: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface SmsCodeRequest {
|
||||
phone: string;
|
||||
}
|
||||
|
||||
export const authApi = {
|
||||
// 微信登录
|
||||
login: (code: string) => {
|
||||
return request<LoginResponse>({
|
||||
url: '/auth/login',
|
||||
@@ -10,6 +20,24 @@ export const authApi = {
|
||||
});
|
||||
},
|
||||
|
||||
// 手机号登录
|
||||
phoneLogin: (data: PhoneLoginRequest) => {
|
||||
return request<LoginResponse>({
|
||||
url: '/auth/phone/login',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
},
|
||||
|
||||
// 发送短信验证码
|
||||
sendSmsCode: (data: SmsCodeRequest) => {
|
||||
return request<void>({
|
||||
url: '/auth/sms/code',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
},
|
||||
|
||||
refreshToken: (refreshToken: string) => {
|
||||
return request<{ accessToken: string }>({
|
||||
url: '/auth/refresh',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Component } from 'react'
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import { Provider } from 'react-redux'
|
||||
import { store } from './store'
|
||||
|
||||
import './app.css'
|
||||
|
||||
@@ -12,7 +14,11 @@ class App extends Component<PropsWithChildren> {
|
||||
componentDidHide() { }
|
||||
|
||||
render() {
|
||||
return this.props.children
|
||||
return (
|
||||
<Provider store={store}>
|
||||
{this.props.children}
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const DEV_API_URL = 'http://localhost:8080';
|
||||
const PROD_API_URL = 'http://localhost:8080';
|
||||
// 生产环境 API 地址,请根据实际部署环境修改
|
||||
const PROD_API_URL = 'https://api.yourdomain.com';
|
||||
|
||||
export const API_BASE_URL = process.env.NODE_ENV === 'development' ? DEV_API_URL : PROD_API_URL;
|
||||
|
||||
@@ -8,6 +8,7 @@ export const useAuth = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { info, token, refreshToken, isLoggedIn, loading } = useAppSelector(state => state.user);
|
||||
|
||||
// 微信登录
|
||||
const login = useCallback(async (code: string) => {
|
||||
dispatch(setLoading(true));
|
||||
try {
|
||||
@@ -18,6 +19,7 @@ export const useAuth = () => {
|
||||
refreshToken: res.refreshToken,
|
||||
}));
|
||||
storage.setToken(res.accessToken);
|
||||
storage.setRefreshToken(res.refreshToken);
|
||||
storage.setUserInfo(res.userInfo);
|
||||
return res;
|
||||
} catch (error) {
|
||||
@@ -28,9 +30,42 @@ export const useAuth = () => {
|
||||
}
|
||||
}, [dispatch]);
|
||||
|
||||
// 手机号登录
|
||||
const phoneLogin = useCallback(async (phone: string, code: string) => {
|
||||
dispatch(setLoading(true));
|
||||
try {
|
||||
const res = await authApi.phoneLogin({ phone, code });
|
||||
dispatch(setUser({
|
||||
user: res.userInfo,
|
||||
token: res.accessToken,
|
||||
refreshToken: res.refreshToken,
|
||||
}));
|
||||
storage.setToken(res.accessToken);
|
||||
storage.setRefreshToken(res.refreshToken);
|
||||
storage.setUserInfo(res.userInfo);
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.error('Phone login failed:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
dispatch(setLoading(false));
|
||||
}
|
||||
}, [dispatch]);
|
||||
|
||||
// 发送短信验证码
|
||||
const sendSmsCode = useCallback(async (phone: string) => {
|
||||
try {
|
||||
await authApi.sendSmsCode({ phone });
|
||||
} catch (error) {
|
||||
console.error('Send SMS code failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
dispatch(logoutAction());
|
||||
storage.removeToken();
|
||||
storage.removeRefreshToken();
|
||||
storage.removeUserInfo();
|
||||
}, [dispatch]);
|
||||
|
||||
@@ -63,6 +98,8 @@ export const useAuth = () => {
|
||||
isLoggedIn,
|
||||
loading,
|
||||
login,
|
||||
phoneLogin,
|
||||
sendSmsCode,
|
||||
logout,
|
||||
refreshToken: refreshAccessToken,
|
||||
updateInfo,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useCallback, useMemo } from "react";
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { View, Text, Button, Input } from "@tarojs/components";
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
import './index.css';
|
||||
|
||||
const Login = () => {
|
||||
@@ -8,8 +9,9 @@ const Login = () => {
|
||||
const [code, setCode] = useState('');
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showNicknameModal, setShowNicknameModal] = useState(false);
|
||||
const [nickname, setNickname] = useState('');
|
||||
|
||||
// 使用封装好的 useAuth hook
|
||||
const { login, phoneLogin, sendSmsCode } = useAuth();
|
||||
|
||||
const handlePhoneInput = useCallback((e: any) => {
|
||||
const value = e.detail.value;
|
||||
@@ -29,7 +31,7 @@ const Login = () => {
|
||||
return '';
|
||||
}, [phone]);
|
||||
|
||||
const handleGetCode = useCallback(() => {
|
||||
const handleGetCode = useCallback(async () => {
|
||||
if (!phone) {
|
||||
Taro.showToast({ title: '请输入手机号', icon: 'none' });
|
||||
return;
|
||||
@@ -38,6 +40,8 @@ const Login = () => {
|
||||
Taro.showToast({ title: '请输入正确的手机号', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await sendSmsCode(phone);
|
||||
setCountdown(60);
|
||||
const timer = setInterval(() => {
|
||||
setCountdown(prev => {
|
||||
@@ -49,7 +53,10 @@ const Login = () => {
|
||||
});
|
||||
}, 1000);
|
||||
Taro.showToast({ title: '验证码已发送', icon: 'success' });
|
||||
}, [phone, isPhoneValid]);
|
||||
} catch (error) {
|
||||
Taro.showToast({ title: '发送失败,请重试', icon: 'none' });
|
||||
}
|
||||
}, [phone, isPhoneValid, sendSmsCode]);
|
||||
|
||||
const handlePhoneLogin = useCallback(async () => {
|
||||
if (!phone) {
|
||||
@@ -66,59 +73,40 @@ const Login = () => {
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
setShowNicknameModal(true);
|
||||
} catch (error) {
|
||||
Taro.showToast({ title: '登录失败', icon: 'none' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [phone, isPhoneValid, code]);
|
||||
|
||||
const handleCreateNickname = useCallback(() => {
|
||||
if (!nickname.trim()) {
|
||||
Taro.showToast({ title: '请输入用户名', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const userInfo = {
|
||||
nickname: nickname.trim(),
|
||||
avatar: '',
|
||||
balance: 0,
|
||||
};
|
||||
Taro.setStorageSync('userInfo', userInfo);
|
||||
await phoneLogin(phone, code);
|
||||
Taro.showToast({ title: '登录成功', icon: 'success' });
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack();
|
||||
}, 1500);
|
||||
}, [nickname]);
|
||||
} catch (error) {
|
||||
Taro.showToast({ title: '登录失败,请重试', icon: 'none' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [phone, isPhoneValid, code, phoneLogin]);
|
||||
|
||||
// 移除 handleCreateNickname 函数,因为现在使用后端登录
|
||||
|
||||
const handleWechatLogin = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 1. 获取微信登录 code
|
||||
const loginResult = await Taro.login();
|
||||
console.log('微信登录 code:', loginResult.code);
|
||||
|
||||
let avatarUrl = '';
|
||||
let nickName = '微信用户';
|
||||
|
||||
// 2. 尝试获取用户信息(可选,用于展示)
|
||||
try {
|
||||
const userInfoRes = await Taro.getUserProfile({
|
||||
await Taro.getUserProfile({
|
||||
desc: '用于完善用户资料',
|
||||
});
|
||||
if (userInfoRes.userInfo) {
|
||||
avatarUrl = userInfoRes.userInfo.avatarUrl;
|
||||
nickName = userInfoRes.userInfo.nickName;
|
||||
}
|
||||
// 用户信息已通过 login() 从后端获取,这里只是请求授权
|
||||
} catch (e) {
|
||||
console.log('游客模式获取用户信息受限,使用默认信息');
|
||||
}
|
||||
|
||||
const userInfo = {
|
||||
nickname: nickName,
|
||||
avatar: avatarUrl,
|
||||
balance: 0,
|
||||
};
|
||||
Taro.setStorageSync('userInfo', userInfo);
|
||||
// 3. 调用后端 API 进行登录
|
||||
await login(loginResult.code);
|
||||
|
||||
Taro.showToast({ title: '登录成功', icon: 'success' });
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack();
|
||||
@@ -129,28 +117,7 @@ const Login = () => {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (showNicknameModal) {
|
||||
return (
|
||||
<View className='login-page'>
|
||||
<View className='nickname-modal'>
|
||||
<Text className='modal-title'>创建用户名</Text>
|
||||
<Text className='modal-subtitle'>请设置一个方便识别的用户名</Text>
|
||||
<Input
|
||||
className='nickname-input'
|
||||
placeholder='请输入用户名'
|
||||
maxlength={20}
|
||||
value={nickname}
|
||||
onInput={(e) => setNickname(e.detail.value)}
|
||||
/>
|
||||
<Button className='confirm-btn' onClick={handleCreateNickname}>
|
||||
完成
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}, [login]);
|
||||
|
||||
return (
|
||||
<View className='login-page'>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { View, Text, Button } from "@tarojs/components";
|
||||
import Taro from '@tarojs/taro';
|
||||
import TabBar from '../../components/TabBar/TabBar';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
import './index.css';
|
||||
|
||||
interface UserInfo {
|
||||
@@ -15,15 +16,18 @@ const UserIndex = () => {
|
||||
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
|
||||
const [tabIndex, setTabIndex] = useState(4); // 用户中心是第5个标签(索引4)
|
||||
|
||||
// 使用封装好的 useAuth hook
|
||||
const { user, isLoggedIn: isAuthLoggedIn, logout } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
checkLoginStatus();
|
||||
}, []);
|
||||
|
||||
const checkLoginStatus = () => {
|
||||
const storedUser = Taro.getStorageSync('userInfo');
|
||||
if (storedUser) {
|
||||
// 使用 Redux store 中的登录状态
|
||||
if (isAuthLoggedIn && user) {
|
||||
setIsLoggedIn(true);
|
||||
setUserInfo(storedUser);
|
||||
setUserInfo(user);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -52,8 +56,8 @@ const UserIndex = () => {
|
||||
content: '确定要退出登录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
Taro.removeStorageSync('userInfo');
|
||||
Taro.removeStorageSync('token');
|
||||
// 使用封装好的 logout 方法
|
||||
logout();
|
||||
setIsLoggedIn(false);
|
||||
setUserInfo(null);
|
||||
Taro.showToast({ title: '已退出登录', icon: 'success' });
|
||||
|
||||
@@ -3,6 +3,16 @@ import { userReducer } from './slices/userSlice';
|
||||
import { orderReducer } from './slices/orderSlice';
|
||||
import { fileReducer } from './slices/fileSlice';
|
||||
import { printerReducer } from './slices/printerSlice';
|
||||
import { storage } from '../utils/storage';
|
||||
|
||||
// 从本地存储恢复用户状态
|
||||
const restoredUserState = {
|
||||
info: storage.getUserInfo() || null,
|
||||
token: storage.getToken() || null,
|
||||
refreshToken: storage.getRefreshToken() || null,
|
||||
isLoggedIn: !!storage.getToken(),
|
||||
loading: false,
|
||||
};
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
@@ -11,6 +21,9 @@ export const store = configureStore({
|
||||
file: fileReducer,
|
||||
printer: printerReducer,
|
||||
},
|
||||
preloadedState: {
|
||||
user: restoredUserState,
|
||||
},
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
|
||||
@@ -10,6 +10,13 @@ interface RequestOptions {
|
||||
header?: Record<string, string>;
|
||||
}
|
||||
|
||||
// 统一响应格式
|
||||
interface ApiResponse<T = any> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export const request = async <T>(options: RequestOptions): Promise<T> => {
|
||||
const { url, method = 'GET', data, header = {} } = options;
|
||||
|
||||
@@ -19,7 +26,7 @@ export const request = async <T>(options: RequestOptions): Promise<T> => {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await Taro.request({
|
||||
const response = await Taro.request<ApiResponse<T>>({
|
||||
url: `${API_BASE_URL}${url}`,
|
||||
method,
|
||||
data,
|
||||
@@ -30,8 +37,17 @@ export const request = async <T>(options: RequestOptions): Promise<T> => {
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
return response.data as T;
|
||||
const apiResponse = response.data;
|
||||
// 按照 API 文档规范检查 code 字段,0 表示成功
|
||||
if (apiResponse.code === 0) {
|
||||
return apiResponse.data;
|
||||
} else {
|
||||
// 处理业务错误
|
||||
console.error('API Error:', apiResponse.message || 'Request failed');
|
||||
throw new Error(apiResponse.message || 'Request failed');
|
||||
}
|
||||
} else if (response.statusCode === 401) {
|
||||
// 未授权,自动登出
|
||||
store.dispatch(logout());
|
||||
throw new Error('Unauthorized');
|
||||
} else {
|
||||
@@ -43,7 +59,7 @@ export const request = async <T>(options: RequestOptions): Promise<T> => {
|
||||
}
|
||||
};
|
||||
|
||||
export const uploadFile = <T>(filePath: string, onProgress?: (progress: number) => void): Promise<T> => {
|
||||
export const uploadFile = <T>(filePath: string): Promise<T> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = store.getState().user.token;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
const USER_TOKEN_KEY = 'user_token';
|
||||
const USER_REFRESH_TOKEN_KEY = 'user_refresh_token';
|
||||
const USER_INFO_KEY = 'user_info';
|
||||
|
||||
export const storage = {
|
||||
@@ -16,6 +17,18 @@ export const storage = {
|
||||
Taro.removeStorageSync(USER_TOKEN_KEY);
|
||||
},
|
||||
|
||||
setRefreshToken: (refreshToken: string) => {
|
||||
Taro.setStorageSync(USER_REFRESH_TOKEN_KEY, refreshToken);
|
||||
},
|
||||
|
||||
getRefreshToken: (): string => {
|
||||
return Taro.getStorageSync(USER_REFRESH_TOKEN_KEY) || '';
|
||||
},
|
||||
|
||||
removeRefreshToken: () => {
|
||||
Taro.removeStorageSync(USER_REFRESH_TOKEN_KEY);
|
||||
},
|
||||
|
||||
setUserInfo: (info: any) => {
|
||||
Taro.setStorageSync(USER_INFO_KEY, info);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user