159 lines
8.5 KiB
Markdown
159 lines
8.5 KiB
Markdown
# 视野 后端服务
|
||
|
||
野生动物实时识别 App「视野」(Flutter 客户端见 `../flutter_app/`)的支付与授权后端。
|
||
|
||
## 功能模块
|
||
|
||
| 模块 | 说明 |
|
||
|---|---|
|
||
| 账号 | 手机号+密码注册/登录(`POST /api/v1/auth/register` / `login`),密码 bcrypt 哈希存储,登录签发自签名 token(`auth.secret`),授权绑定手机号账号、同账号多设备共享 |
|
||
| 订单 | 客户端 `GET /api/v1/plans` 拉取套餐价格 → `POST /api/v1/orders` 创建订单并返回支付参数(微信 prepay / 支付宝 orderStr);未支付订单 2 小时惰性关闭,同账号 2 小时内复用 created 订单防重复扣款 |
|
||
| 支付回调 | 微信支付 / 支付宝异步回调验签落授权(`/api/v1/payment` 分组,按渠道应答格式直返,不做统一包装) |
|
||
| 订单确认 | 客户端支付成功后 `POST /api/v1/orders/{orderId}/confirm` 幂等通知,加速授权刷新 |
|
||
| 授权查询 | `GET /api/v1/license`(Bearer token)返回授权状态,App 识别入口强制服务端校验 |
|
||
| 套餐 | `config.yml` `plans` 节点配置三档套餐(改价 = 改配置重启),价格**整数分**(1000 / 5600 / 18000) |
|
||
| 后台管理端 | `server_admin/`(Vue3 + Element Plus)管理页面:订单查询、账号/授权管理(手动授权/撤销);构建产物由后端 `/admin/` 托管,登录页输入 token 后以 `X-Admin-Token` 头鉴权(`config.yml admin.token`) |
|
||
|
||
## 架构与数据流
|
||
|
||
```
|
||
Flutter App ── POST /auth/register|login ─► 账号注册/登录,签发 token
|
||
│
|
||
├─► POST /orders (Bearer token) ──► 后端下单(校验 planId/channel)
|
||
│ │
|
||
│◄───────┘ orderId + payParams
|
||
│
|
||
├─► 拉起微信/支付宝 SDK 支付
|
||
│
|
||
├─► 微信/支付宝服务端异步回调 ─► 验签 → 落授权(幂等,绑账号)
|
||
│
|
||
└─► POST /orders/{id}/confirm (Bearer token) ─► 幂等确认(回调已落授权则直接返回)
|
||
│
|
||
└─► GET /license (Bearer token) ─► 返回 {active, expiresAt}
|
||
```
|
||
|
||
- 授权以**服务端为准**:客户端本地缓存 `expiresAt` 仅用于主页展示,识别入口强制查询服务端
|
||
- 有效期语义:**自然日**(day 当天 24:00 失效;week/month 自生效日起自然日累计),服务端计算 `expiresAt`,客户端只读不判权
|
||
|
||
## 数据表
|
||
|
||
| 表 | 说明 | 关键字段 |
|
||
|---|---|---|
|
||
| `payment_order` | 支付订单 | `order_id`(PK)、`phone_num`、`plan_id`、`channel`(wechat/alipay)、`amount_cents`、`status`(created/paid/closed)、`wx_trade_no`(UNIQUE)、`alipay_trade_no`(UNIQUE)、`created_at`、`paid_at` |
|
||
| `license` | 手机号账号与授权 | `phone_num`(PK)、`password`(bcrypt)、`expires_at`(未充值 NULL)、`created_at`、`updated_at` |
|
||
|
||
建表与迁移见 `技术设计.md`(新库直接建表;存量库以 `PRAGMA user_version` 版本化迁移)。
|
||
|
||
## API 清单
|
||
|
||
响应统一格式 `{"code": 0, "message": "ok", "data": {...}}`,`code != 0` 视为失败。接口只允许 GET / POST。
|
||
|
||
### POST /api/v1/auth/register
|
||
|
||
手机号注册(与登录无关的公开接口,无需 token)。请求:`{"phone": "13800000000", "password": "..."}`。已注册手机号返回错误。注册成功即创建账号(未充值,无额度)。
|
||
|
||
### POST /api/v1/auth/login
|
||
|
||
手机号登录。请求:`{"phone": "13800000000", "password": "..."}`。响应 `data: {"token": "..."}`(HMAC 自签名,有效期 `auth.token_ttl`,默认 30 天)。后续登录态接口携带 `Authorization: Bearer <token>`。
|
||
|
||
### GET /api/v1/plans
|
||
|
||
套餐列表(需 Bearer token)。响应 `data`:
|
||
|
||
```json
|
||
{
|
||
"list": [
|
||
{"planId": "day", "days": 1, "priceCents": 1000},
|
||
{"planId": "week", "days": 7, "priceCents": 5600},
|
||
{"planId": "month", "days": 30, "priceCents": 18000}
|
||
]
|
||
}
|
||
```
|
||
|
||
套餐来自 `config.yml` `plans` 节点(静态定价,改价改配置后重启服务生效);客户端不硬编码价格。展示名由客户端按 `days` 拼「N天」派生,无独立 label 字段。
|
||
|
||
### POST /api/v1/orders
|
||
|
||
创建订单,返回渠道支付参数(需 Bearer token,手机号由 token 解出)。
|
||
|
||
请求(JSON body):
|
||
|
||
```json
|
||
{"planId": "week", "channel": "wechat"}
|
||
```
|
||
|
||
- `channel`:`wechat` | `alipay`
|
||
- `planId`:`day` | `week` | `month`(需在 `config.yml` `plans` 节点存在,金额由服务端按配置锁定,客户端不可改价)
|
||
|
||
响应 `data`:
|
||
|
||
```json
|
||
{
|
||
"orderId": "O20260822001",
|
||
"payParams": {
|
||
"partnerId": "1900xxxxx",
|
||
"prepayId": "wx...",
|
||
"nonceStr": "...",
|
||
"timeStamp": "1728000000",
|
||
"sign": "...",
|
||
"packageValue": "Sign=WXPay"
|
||
}
|
||
}
|
||
```
|
||
|
||
- `channel == wechat`:`payParams` 为微信 APP 支付统一下单返回参数(后端签名)
|
||
- `channel == alipay`:`payParams` 为 `{"orderStr": "alipay_sdk=..."}`(后端调 `alipay.trade.app.pay` 生成)
|
||
|
||
### POST /api/v1/orders/{orderId}/confirm
|
||
|
||
客户端支付成功后通知(幂等,需 Bearer token)。服务端以异步回调为准落授权,confirm 仅用于加速刷新;回调已到账时直接返回成功。
|
||
|
||
请求:`{}`(无参数)。响应:`data: {"status": "paid"|"created"}`。
|
||
|
||
返回 `created`(回调未到账)时客户端按 3s → 10s → 30s 递增重试 confirm(最多 3 次),期间禁止重新下单;仍未成功回退 `GET /license` 查询(回调到账后自然变为 active)。回调为唯一授权来源,confirm 仅加速刷新。
|
||
|
||
### GET /api/v1/license
|
||
|
||
查询账号授权状态(需 Bearer token,手机号由 token 解出)。
|
||
|
||
响应 `data`:
|
||
|
||
```json
|
||
{"active": true, "expiresAt": "2026-08-29T23:59:59+08:00"}
|
||
```
|
||
|
||
`active: false` 或 `expiresAt` 已过期(含未充值账号)→ 客户端展示付费墙/充值引导。
|
||
|
||
### 支付回调(微信/支付宝,勿直连)
|
||
|
||
| 渠道 | 路径 | 说明 |
|
||
|---|---|---|
|
||
| 微信支付(APP 支付 V3) | `POST /api/v1/payment/wechat/notify` | 解密+验签回调,`out_trade_no` → 落授权 |
|
||
| 支付宝(APP 支付) | `POST /api/v1/payment/alipay/notify` | RSA2 验签回调,`out_trade_no` → 落授权 |
|
||
|
||
### 管理端接口(`/api/v1/admin`,需请求头 `X-Admin-Token` = `config.yml admin.token`)
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
|---|---|---|
|
||
| GET | `/admin/orders` | 订单列表,筛选参数 `phoneNum/status/startAt/endAt` + `page/size`,返回 `{total, list}` |
|
||
| GET | `/admin/licenses` | 账号/授权列表,筛选 `phoneNum` + `page/size`(含未充值账号) |
|
||
| POST | `/admin/licenses/grant` | 手动授权 `{"phoneNum":"13800000000","planId":"day"}`,自然日叠加 |
|
||
| POST | `/admin/licenses/revoke` | 撤销授权 `{"phoneNum":"13800000000"}`(清授权保留账号) |
|
||
|
||
管理页面(订单/授权)由 `server_admin/` 构建产物提供,访问 `http://<host>/admin/`。金额均为整数分,前端展示 ÷100 转元。
|
||
|
||
## 使用说明
|
||
|
||
1. 配置 `config.yml`:监听端口、数据库路径、登录 token 签名密钥 `auth.secret`(必填,换值即全员下线)、套餐 `plans` 节点、微信支付(appid/mchid/商户私钥/证书序列号/APIv3 密钥)、支付宝(appid/应用私钥/支付宝公钥)、管理端 `admin.token`;SQLite 库由服务启动时自动建表并迁移,无需手工初始化
|
||
2. `go build ./...` 编译验证
|
||
3. 本地运行 `go run main.go`;服务层白盒测试:`GF_GCFG_FILE=biz/service/testdata/config.yml go test ./biz/service/`(独立测试库,见 `biz/service/testdata/`)
|
||
4. 后台管理端:`cd server_admin && npm run build`(构建产物输出到 `server/admin_dist/`,由后端 `/admin/` 托管);开发联调 `npm run dev`(Vite 代理 `/api` → `:8080`)。首次访问 `/admin/` 进入登录页,输入 `config.yml admin.token` 对应的管理 token(存浏览器 localStorage,随请求携带;token 不内嵌构建产物)
|
||
5. Docker Compose 部署见 `Dockerfile` / `docker-compose.yml`(`./data` 运行时数据目录挂载持久化,容器重建不丢数据)
|
||
6. 客户端对接:`flutter_app/lib/config/app_config.dart` 的 `apiBaseUrl`、微信/支付宝 AppID 与 URL Scheme 替换为真实值
|
||
|
||
## 金额与安全约定
|
||
|
||
- 金额一律**整数分**(`price_cents` / `amount_cents` 为 INT64),禁止浮点元;前端展示 ÷100
|
||
- 支付回调必须**验签**(微信 APIv3 验签/解密、支付宝 RSA2),回调处理**幂等**(`wx_trade_no`/`alipay_trade_no` 唯一约束,重复回调直接忽略)
|
||
- 授权落库以**较晚到期为准**:已有授权未过期时新套餐顺延叠加,禁止覆盖缩短
|