Compare commits
2 Commits
eb2f6554b2
...
b193759e90
| Author | SHA1 | Date | |
|---|---|---|---|
| b193759e90 | |||
| eb941c06c0 |
@@ -16,6 +16,7 @@ const handler = new OpenAPIHandler(router, {
|
|||||||
info: {
|
info: {
|
||||||
title: name,
|
title: name,
|
||||||
version,
|
version,
|
||||||
|
description: 'UX 授权服务 OpenAPI 文档:设备授权、任务解密、摘要加密与报告签名打包接口。',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
docsPath: '/docs',
|
docsPath: '/docs',
|
||||||
|
|||||||
@@ -2,65 +2,103 @@ import { oc } from '@orpc/contract'
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
export const encryptDeviceInfo = oc
|
export const encryptDeviceInfo = oc
|
||||||
|
.route({
|
||||||
|
method: 'POST',
|
||||||
|
path: '/crypto/encrypt-device-info',
|
||||||
|
operationId: 'encryptDeviceInfo',
|
||||||
|
summary: '生成设备授权密文',
|
||||||
|
description:
|
||||||
|
'按 deviceId 查询已注册设备,使用设备记录中的 platformPublicKey 对 {licence, fingerprint} 做 RSA-OAEP 加密,返回 Base64 密文。',
|
||||||
|
tags: ['Crypto'],
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
deviceId: z.string().min(1),
|
deviceId: z.string().min(1).describe('设备 ID'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(
|
.output(
|
||||||
z.object({
|
z.object({
|
||||||
encrypted: z.string(),
|
encrypted: z.string().describe('Base64 密文(用于设备授权二维码)'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const decryptTask = oc
|
export const decryptTask = oc
|
||||||
|
.route({
|
||||||
|
method: 'POST',
|
||||||
|
path: '/crypto/decrypt-task',
|
||||||
|
operationId: 'decryptTask',
|
||||||
|
summary: '解密任务二维码',
|
||||||
|
description:
|
||||||
|
'按 deviceId 查询已注册设备,使用 UTF-8 编码下的 licence 与 fingerprint 直接拼接(无分隔符)后取 SHA256 作为 AES-256-GCM 密钥解密任务密文。',
|
||||||
|
tags: ['Crypto'],
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
deviceId: z.string().min(1),
|
deviceId: z.string().min(1).describe('设备 ID'),
|
||||||
encryptedData: z.string().min(1),
|
encryptedData: z.string().min(1).describe('任务二维码中的 Base64 密文'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(
|
.output(
|
||||||
z.object({
|
z.object({
|
||||||
taskId: z.string(),
|
taskId: z.string().describe('任务 ID'),
|
||||||
enterpriseId: z.string(),
|
enterpriseId: z.string().describe('企业 ID'),
|
||||||
orgName: z.string(),
|
orgName: z.string().describe('单位名称'),
|
||||||
inspectionId: z.string(),
|
inspectionId: z.string().describe('检查 ID'),
|
||||||
inspectionPerson: z.string(),
|
inspectionPerson: z.string().describe('检查人'),
|
||||||
issuedAt: z.number(),
|
issuedAt: z.number().describe('任务发布时间戳(毫秒)'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const encryptSummary = oc
|
export const encryptSummary = oc
|
||||||
|
.route({
|
||||||
|
method: 'POST',
|
||||||
|
path: '/crypto/encrypt-summary',
|
||||||
|
operationId: 'encryptSummary',
|
||||||
|
summary: '加密摘要二维码内容',
|
||||||
|
description: '按 deviceId 查询已注册设备,使用 HKDF-SHA256 + AES-256-GCM 加密摘要信息,返回二维码 JSON 字符串。',
|
||||||
|
tags: ['Crypto'],
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
deviceId: z.string().min(1),
|
deviceId: z.string().min(1).describe('设备 ID'),
|
||||||
taskId: z.string().min(1),
|
taskId: z.string().min(1).describe('任务 ID'),
|
||||||
enterpriseId: z.string().min(1),
|
enterpriseId: z.string().min(1).describe('企业 ID'),
|
||||||
inspectionId: z.string().min(1),
|
inspectionId: z.string().min(1).describe('检查 ID'),
|
||||||
summary: z.string().min(1),
|
summary: z.string().min(1).describe('摘要明文'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(
|
.output(
|
||||||
z.object({
|
z.object({
|
||||||
qrContent: z.string(),
|
qrContent: z.string().describe('二维码内容 JSON:{"taskId":"...","encrypted":"..."}'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const signAndPackReport = oc
|
export const signAndPackReport = oc
|
||||||
|
.route({
|
||||||
|
method: 'POST',
|
||||||
|
path: '/crypto/sign-and-pack-report',
|
||||||
|
operationId: 'signAndPackReport',
|
||||||
|
summary: '签名并打包报告 ZIP',
|
||||||
|
description:
|
||||||
|
'接收原始 ZIP(multipart/form-data 文件字段 rawZip),由 UX 生成 summary.json、manifest.json、signature.asc,并返回 signedZipBase64。',
|
||||||
|
tags: ['Crypto', 'Report'],
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
deviceId: z.string().min(1),
|
deviceId: z.string().min(1).describe('设备 ID'),
|
||||||
taskId: z.string().min(1),
|
taskId: z.string().min(1).describe('任务 ID'),
|
||||||
enterpriseId: z.string().min(1),
|
enterpriseId: z.string().min(1).describe('企业 ID'),
|
||||||
inspectionId: z.string().min(1),
|
inspectionId: z.string().min(1).describe('检查 ID'),
|
||||||
summary: z.string().min(1),
|
summary: z.string().min(1).describe('检查摘要明文'),
|
||||||
rawZip: z.file().mime(['application/zip', 'application/x-zip-compressed']),
|
rawZip: z
|
||||||
|
.file()
|
||||||
|
.mime(['application/zip', 'application/x-zip-compressed'])
|
||||||
|
.describe('原始报告 ZIP 文件(multipart/form-data 字段)'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(
|
.output(
|
||||||
z.object({
|
z.object({
|
||||||
deviceSignature: z.string(),
|
deviceSignature: z.string().describe('设备签名(HMAC-SHA256 Base64)'),
|
||||||
signedZipBase64: z.string(),
|
signedZipBase64: z.string().describe('签名后 ZIP 的 Base64 编码'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,29 +2,45 @@ import { oc } from '@orpc/contract'
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
const deviceOutput = z.object({
|
const deviceOutput = z.object({
|
||||||
id: z.string(),
|
id: z.string().describe('设备主键 ID'),
|
||||||
licence: z.string(),
|
licence: z.string().describe('设备授权码 licence'),
|
||||||
fingerprint: z.string(),
|
fingerprint: z.string().describe('UX 计算并持久化的设备指纹'),
|
||||||
platformPublicKey: z.string(),
|
platformPublicKey: z.string().describe('平台公钥(Base64,SPKI DER)'),
|
||||||
pgpPublicKey: z.string().nullable(),
|
pgpPublicKey: z.string().nullable().describe('设备 OpenPGP 公钥(ASCII armored)'),
|
||||||
createdAt: z.date(),
|
createdAt: z.date().describe('记录创建时间'),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date().describe('记录更新时间'),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const register = oc
|
export const register = oc
|
||||||
|
.route({
|
||||||
|
method: 'POST',
|
||||||
|
path: '/device/register',
|
||||||
|
operationId: 'deviceRegister',
|
||||||
|
summary: '注册设备',
|
||||||
|
description: '注册 licence 与平台公钥,指纹由 UX 本机计算,返回设备信息。',
|
||||||
|
tags: ['Device'],
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
licence: z.string().min(1),
|
licence: z.string().min(1).describe('设备授权码 licence'),
|
||||||
platformPublicKey: z.string().min(1),
|
platformPublicKey: z.string().min(1).describe('平台公钥(Base64,SPKI DER)'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(deviceOutput)
|
.output(deviceOutput)
|
||||||
|
|
||||||
export const get = oc
|
export const get = oc
|
||||||
|
.route({
|
||||||
|
method: 'POST',
|
||||||
|
path: '/device/get',
|
||||||
|
operationId: 'deviceGet',
|
||||||
|
summary: '查询设备',
|
||||||
|
description: '按 id 或 licence 查询设备信息。',
|
||||||
|
tags: ['Device'],
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string().optional(),
|
id: z.string().optional().describe('设备 ID,与 licence 二选一'),
|
||||||
licence: z.string().optional(),
|
licence: z.string().optional().describe('设备授权码,与 id 二选一'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(deviceOutput)
|
.output(deviceOutput)
|
||||||
|
|||||||
@@ -2,46 +2,70 @@ import { oc } from '@orpc/contract'
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
const taskOutput = z.object({
|
const taskOutput = z.object({
|
||||||
id: z.string(),
|
id: z.string().describe('任务记录 ID'),
|
||||||
deviceId: z.string(),
|
deviceId: z.string().describe('设备 ID'),
|
||||||
taskId: z.string(),
|
taskId: z.string().describe('任务业务 ID'),
|
||||||
enterpriseId: z.string().nullable(),
|
enterpriseId: z.string().nullable().describe('企业 ID'),
|
||||||
orgName: z.string().nullable(),
|
orgName: z.string().nullable().describe('单位名称'),
|
||||||
inspectionId: z.string().nullable(),
|
inspectionId: z.string().nullable().describe('检查 ID'),
|
||||||
inspectionPerson: z.string().nullable(),
|
inspectionPerson: z.string().nullable().describe('检查人'),
|
||||||
issuedAt: z.date().nullable(),
|
issuedAt: z.date().nullable().describe('任务发布时间(ISO date-time;由毫秒时间戳转换后存储)'),
|
||||||
status: z.enum(['pending', 'in_progress', 'done']),
|
status: z.enum(['pending', 'in_progress', 'done']).describe('任务状态'),
|
||||||
createdAt: z.date(),
|
createdAt: z.date().describe('记录创建时间'),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date().describe('记录更新时间'),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const save = oc
|
export const save = oc
|
||||||
|
.route({
|
||||||
|
method: 'POST',
|
||||||
|
path: '/task/save',
|
||||||
|
operationId: 'taskSave',
|
||||||
|
summary: '保存任务',
|
||||||
|
description: '保存解密后的任务信息到 UX 数据库。',
|
||||||
|
tags: ['Task'],
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
deviceId: z.string().min(1),
|
deviceId: z.string().min(1).describe('设备 ID'),
|
||||||
taskId: z.string().min(1),
|
taskId: z.string().min(1).describe('任务 ID'),
|
||||||
enterpriseId: z.string().optional(),
|
enterpriseId: z.string().optional().describe('企业 ID'),
|
||||||
orgName: z.string().optional(),
|
orgName: z.string().optional().describe('单位名称'),
|
||||||
inspectionId: z.string().optional(),
|
inspectionId: z.string().optional().describe('检查 ID'),
|
||||||
inspectionPerson: z.string().optional(),
|
inspectionPerson: z.string().optional().describe('检查人'),
|
||||||
issuedAt: z.number().optional(),
|
issuedAt: z.number().optional().describe('任务发布时间戳(毫秒)'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(taskOutput)
|
.output(taskOutput)
|
||||||
|
|
||||||
export const list = oc
|
export const list = oc
|
||||||
|
.route({
|
||||||
|
method: 'POST',
|
||||||
|
path: '/task/list',
|
||||||
|
operationId: 'taskList',
|
||||||
|
summary: '查询任务列表',
|
||||||
|
description: '按设备 ID 查询任务列表。',
|
||||||
|
tags: ['Task'],
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
deviceId: z.string().min(1),
|
deviceId: z.string().min(1).describe('设备 ID'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(z.array(taskOutput))
|
.output(z.array(taskOutput))
|
||||||
|
|
||||||
export const updateStatus = oc
|
export const updateStatus = oc
|
||||||
|
.route({
|
||||||
|
method: 'POST',
|
||||||
|
path: '/task/update-status',
|
||||||
|
operationId: 'taskUpdateStatus',
|
||||||
|
summary: '更新任务状态',
|
||||||
|
description: '按记录 ID 更新任务状态。',
|
||||||
|
tags: ['Task'],
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string().min(1),
|
id: z.string().min(1).describe('任务记录 ID'),
|
||||||
status: z.enum(['pending', 'in_progress', 'done']),
|
status: z.enum(['pending', 'in_progress', 'done']).describe('目标状态'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(taskOutput)
|
.output(taskOutput)
|
||||||
|
|||||||
103
docs/第三方-OpenAPI-对接指南.md
Normal file
103
docs/第三方-OpenAPI-对接指南.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# 第三方 OpenAPI 对接指南
|
||||||
|
|
||||||
|
本文档用于第三方系统快速接入 UX 授权服务。
|
||||||
|
|
||||||
|
## 1. 文档入口
|
||||||
|
|
||||||
|
- OpenAPI 文档(Scalar):`/api/docs`
|
||||||
|
- OpenAPI 规范(JSON):`/api/spec.json`
|
||||||
|
|
||||||
|
例如本地开发环境:
|
||||||
|
|
||||||
|
- `http://localhost:3000/api/docs`
|
||||||
|
- `http://localhost:3000/api/spec.json`
|
||||||
|
|
||||||
|
## 2. 接口分组
|
||||||
|
|
||||||
|
OpenAPI 中已按 `tags` 分组:
|
||||||
|
|
||||||
|
- `Device`:设备注册与查询
|
||||||
|
- `Crypto`:授权加解密与二维码数据处理
|
||||||
|
- `Report`:报告签名打包
|
||||||
|
- `Task`:任务保存与状态管理
|
||||||
|
|
||||||
|
## 3. 核心接口一览
|
||||||
|
|
||||||
|
### Device
|
||||||
|
|
||||||
|
- `POST /api/device/register`
|
||||||
|
- 操作名:`deviceRegister`
|
||||||
|
- 说明:注册设备,UX 计算并存储 fingerprint
|
||||||
|
- `POST /api/device/get`
|
||||||
|
- 操作名:`deviceGet`
|
||||||
|
- 说明:按 `id` 或 `licence` 查询设备
|
||||||
|
|
||||||
|
### Crypto
|
||||||
|
|
||||||
|
- `POST /api/crypto/encrypt-device-info`
|
||||||
|
- 操作名:`encryptDeviceInfo`
|
||||||
|
- 说明:生成设备授权二维码密文
|
||||||
|
- `POST /api/crypto/decrypt-task`
|
||||||
|
- 操作名:`decryptTask`
|
||||||
|
- 说明:解密任务二维码数据
|
||||||
|
- `POST /api/crypto/encrypt-summary`
|
||||||
|
- 操作名:`encryptSummary`
|
||||||
|
- 说明:加密摘要并返回二维码内容
|
||||||
|
- `POST /api/crypto/sign-and-pack-report`
|
||||||
|
- 操作名:`signAndPackReport`
|
||||||
|
- 说明:上传原始 ZIP,返回签名后 ZIP
|
||||||
|
|
||||||
|
### Task
|
||||||
|
|
||||||
|
- `POST /api/task/save`
|
||||||
|
- 操作名:`taskSave`
|
||||||
|
- `POST /api/task/list`
|
||||||
|
- 操作名:`taskList`
|
||||||
|
- `POST /api/task/update-status`
|
||||||
|
- 操作名:`taskUpdateStatus`
|
||||||
|
|
||||||
|
## 4. 字段说明来源
|
||||||
|
|
||||||
|
每个接口的字段定义、必填/可选、类型、以及业务描述,均在 OpenAPI 中由 oRPC 合约的 Zod schema 自动生成。
|
||||||
|
|
||||||
|
你可以在 `/api/docs` 页面直接查看:
|
||||||
|
|
||||||
|
1. 接口名称(operationId)
|
||||||
|
2. 接口摘要(summary)
|
||||||
|
3. 详细说明(description)
|
||||||
|
4. 请求字段(含描述)
|
||||||
|
5. 响应字段(含描述)
|
||||||
|
|
||||||
|
## 5. 文件上传接口注意事项
|
||||||
|
|
||||||
|
`signAndPackReport` 使用 `multipart/form-data`,文件字段名为 `rawZip`。
|
||||||
|
|
||||||
|
- 文件类型:`application/zip` 或 `application/x-zip-compressed`
|
||||||
|
- 其他业务字段(如 `deviceId`、`taskId`)与文件一起提交
|
||||||
|
- 接口响应为 JSON,其中 `signedZipBase64` 为签名后 ZIP 的 Base64 编码
|
||||||
|
|
||||||
|
示例(curl):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:3000/api/crypto/sign-and-pack-report" \
|
||||||
|
-F "deviceId=dev_xxx" \
|
||||||
|
-F "taskId=TASK-20260115-4875" \
|
||||||
|
-F "enterpriseId=1173040813421105152" \
|
||||||
|
-F "inspectionId=702286470691215417" \
|
||||||
|
-F "summary=检查摘要信息" \
|
||||||
|
-F "rawZip=@./report-raw.zip;type=application/zip"
|
||||||
|
```
|
||||||
|
|
||||||
|
响应中的 `signedZipBase64` 可按以下方式还原为 ZIP:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# jq -r '.signedZipBase64' response.json | base64 -d > signed-report.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 推荐接入方式
|
||||||
|
|
||||||
|
第三方如需代码生成,建议直接消费 `/api/spec.json`:
|
||||||
|
|
||||||
|
- Java:OpenAPI Generator
|
||||||
|
- C#:NSwag / OpenAPI Generator
|
||||||
|
- TypeScript:openapi-typescript / Orval
|
||||||
Reference in New Issue
Block a user