# 视野 后端服务 野生动物实时识别 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 `。 ### 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:///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` 唯一约束,重复回调直接忽略) - 授权落库以**较晚到期为准**:已有授权未过期时新套餐顺延叠加,禁止覆盖缩短