8.6 KiB
视野 后端服务
野生动物实时识别 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)、remark(管理端备注)、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:
{
"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):
{"planId": "week", "channel": "wechat"}
channel:wechat|alipayplanId:day|week|month(需在config.ymlplans节点存在,金额由服务端按配置锁定,客户端不可改价)
响应 data:
{
"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:
{"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"}(清授权保留账号) |
| POST | /admin/licenses/remark |
写账号备注 {"phoneNum":"13800000000","remark":"..."}(空串清空,≤200 字) |
管理页面(订单/授权)由 server_admin/ 构建产物提供,访问 http://<host>/admin/。金额均为整数分,前端展示 ÷100 转元。
使用说明
- 配置
config.yml:监听端口、数据库路径、登录 token 签名密钥auth.secret(必填,换值即全员下线)、套餐plans节点、微信支付(appid/mchid/商户私钥/证书序列号/APIv3 密钥)、支付宝(appid/应用私钥/支付宝公钥)、管理端admin.token;SQLite 库由服务启动时自动建表并迁移,无需手工初始化 go build ./...编译验证- 本地运行
go run main.go;服务层白盒测试:GF_GCFG_FILE=biz/service/testdata/config.yml go test ./biz/service/(独立测试库,见biz/service/testdata/) - 后台管理端:
cd server_admin && npm run build(构建产物输出到server/admin_dist/,由后端/admin/托管);开发联调npm run dev(Vite 代理/api→:8080)。首次访问/admin/进入登录页,输入config.yml admin.token对应的管理 token(存浏览器 localStorage,随请求携带;token 不内嵌构建产物) - Docker Compose 部署见
Dockerfile/docker-compose.yml(./data运行时数据目录挂载持久化,容器重建不丢数据) - 客户端对接:
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唯一约束,重复回调直接忽略) - 授权落库以较晚到期为准:已有授权未过期时新套餐顺延叠加,禁止覆盖缩短