Files
observer/server
2026-08-28 17:04:23 +08:00
..
1
2026-08-28 17:04:23 +08:00
1
2026-08-28 17:04:23 +08:00
1
2026-08-28 14:31:13 +08:00
1
2026-08-28 17:04:23 +08:00
1
2026-08-27 11:14:38 +08:00
1
2026-08-28 17:04:23 +08:00
1
2026-08-28 17:04:23 +08:00
2026-08-26 18:22:56 +08:00
2026-08-26 18:22:56 +08:00
1
2026-08-27 11:04:56 +08:00
1
2026-08-28 17:04:23 +08:00
1
2026-08-28 17:04:23 +08:00

视野 后端服务

野生动物实时识别 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/licenseBearer token)返回授权状态,App 识别入口强制服务端校验
套餐 config.yml plans 节点配置三档套餐(改价 = 改配置重启),价格整数分1000 / 5600 / 18000
后台管理端 server_admin/Vue3 + Element Plus)管理页面:订单查询、账号/授权管理(手动授权/撤销)、App 版本管理;构建产物由后端 /admin/ 托管,登录页输入 token 后以 X-Admin-Token 头鉴权(config.yml admin.token
版本管理 后台管理端上传 Android APK + 更新说明,APK 存服务器 app.apkDir(默认 ./workspace/,与 ./data 平级、挂载持久化)固定文件名 observer-latest.apk,上传即覆盖,目录永远只保留最新一个文件版本号从文件名识别:文件须命名为 observer-x.y.z.apk(Flutter 打包产物即此命名,版本号取自 pubspec);客户端启动时 GET /api/v1/app/update 检查更新:服务器版本高于本地版本即弹更新提示(不可跳过)。仅 Android 检查,iOS 不做版本下发iOS 走 App Store 自行更新)。版本记录可删除:删最新版本联动删除 APK 文件,删历史版本仅删记录
数据训练(唯一入口) 后台管理端「数据训练」一个菜单承载数据集全流程:数据集卡片列表(封面图/描述/图片数/已标注数/训练状态徽标),卡片下方直接展示训练任务进度条与状态(无独立训练页);详情页为图片与标注一体视图:分页(每页 20 条)逐行「原图 ‖ 标注图」对照展示;图片入库(手动上传/AI 生成)自动触发 RF-DETR 全图扫描标注,进度条展示在页顶;页顶另有「全量标注」按钮可手动重标全部图片(覆盖各图已有标注);点击原图/标注图弹窗放大,弹窗内 canvas 直接画框/点框删除/清空并保存——AI 自动标注结果直接作为标注,人工可修改/清理全部框;封面(上传自动转 jpg + UUID 命名)/描述/AI 生成图片(provider 抽象:dashscope 通义万相付费 API / localai 训练机 local-ai qwen-imageconfig.yml imageGen 节点切换,见配置说明);AI 标注端点与训练机 SSH 为全局配置,直接读 config.ymllocalAi / training.ssh 节点,改配置需重启服务);图片落服务器 app.datasetDir/datasets/<数据集名>/DB 存元数据 + 标注 JSON
模型训练 从数据集卡片「开始训练」一键触发(参数 imgsz/epochs/batch/device 默认走 config.yml training 节点,部署级配置):进度/日志/指标监控(每 epoch 粒度)、取消;训练通道 training 节点可配置 subprocess(与 Go 服务同机直接起 python)/ ssh(异机执行,SSH 凭据取 config.yml training.ssh 节点),并发度 1(GPU 独占);训练脚本 server/training/train_server.py(随项目迁移,2026-08-26)参数化,产物(best.tflite/best.pt/曲线)拉回服务器;训练收尾自动做 tflite 产物自检(输入/输出 shape 校验,原 inspect_tflite.py 逻辑内嵌脚本),自检失败任务置失败并带出原因;dump_graph.py 留作训练机人工深度调试
模型版本与热更新 每数据集一个模型:训练完成后一键「发布」(训练任务操作列)——tflite 落 workspace/trainings/<数据集名>.tflite + sha256/指标/类别名入 model_version(按数据集独立版本序列 m1.0.0 递增)。管理端无模型管理界面(版本记录仅支撑客户端下发)。App 模型热更新GET /api/v1/app/update 扩展返回 models 目录数组,客户端独立检查,新模型下载校验替换,失败回退旧模型——模型迭代不再重打包 APK
模型目录与多模型推理 GET /api/v1/models(登录态)返回全部数据集当前生效模型(数据集/版本/类别/大小/sha256/下载地址),下载 URL /download/trainings/<数据集名>.tfliteApp 模型管理页用户自由下载/删除/启用模型,识别时加载全部已启用模型并行推理 + 跨模型 NMS 合并(按类别名),内置 assets 模型兜底
标注 图片入库自动触发:手动上传/AI 生成成功后,新增图自动调 config.yml localAi 节点配置的 AI 端点做 RF-DETR 全图扫描(label_task 记录进度,页顶进度条展示;localAi 未配置 → 上传/生成接口直接报错;已有标注任务在跑(忙)→ 不报错,当前任务成功完成后自动补标未标注图)→ 扫描结果(重叠去重:NMS 风格按置信度降序保留,重叠比 > localAi.overlapThreshold 默认 0.3 的框剔除——重叠比 = 交叠面积/两框较小面积,RF-DETR 同目标常输出一大一小两框,此判据能命中,同目标只留置信度最高者)直接写 dataset_image.labels_json(覆盖该图已有标注,即重标语义);点击弹窗放大后在 canvas 上画框/点框删除/清空/改类别 → 保存即整体覆写 dataset_image.labels_jsonYOLO 归一化 JSON 数组,AI 与人工框同存,人工可修改/清理);POST /admin/label-tasks 详情页「全量标注」按钮入口(另有自动触发),可发起全量/指定图重标;自动/手动/混合并存,训练前自动整理(prepare_yolo 逻辑在服务端)

架构与数据流

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_numplan_idchannel(wechat/alipay)、amount_centsstatus(created/paid/closed)、wx_trade_no(UNIQUE)、alipay_trade_no(UNIQUE)、created_atpaid_at
license 手机号账号与授权 phone_num(PK)、password(bcrypt)、expires_at(未充值 NULL)、remark(管理端备注)、created_atupdated_at
app_version App 版本管理 id(PK)、version(x.y.z, UNIQUE)、notes(更新说明)、created_atupdated_at(下载地址不落表:APK 固定文件 app.apkDir/observer-latest.apk,默认 ./workspace/
dataset 训练数据集 id(PK)、name(UNIQUE)、source(manual/ai)、image_countlabeled_countstatus(building/synced/labeled)、cover(封面文件名,UUID 命名 jpg,如 9f2a...-xx.jpg)、descriptioncreated_atupdated_at(图片文件在 app.datasetDir/datasets/<name>/,DB 只存元数据;AI 标注/训练机 SSH 配置走 config.ymllocalAi / training.ssh 节点)
dataset_image 数据集图片 id(PK)、dataset_idfilenamesource(manual/ai)、prompt(AI 生成图记录提示词)、labels_json(标注 JSON 数组:YOLO 归一化 xywh+类别+置信度,AI 自动标注与人工标注同存、人工可修改/清理,null/''/'[]'=未标注)、created_at
model_training 训练任务 id(PK)、namestatus(running/success/failed)、dataset(训练机数据集名)、imgsz/epochs/batch/device(参数快照)、current_epoch/total_epochsmetrics(JSON)、log_tailpiderrorstarted_at/finished_atcreated_at
model_version 模型版本(每数据集独立序列) id(PK)、dataset_idversion(m1.0.0 递增, 同数据集 UNIQUE)、training_idmetrics(JSON)、labels(JSON 类别名数组)、sha256size_bytesis_latestnotescreated_at(模型文件不落表:发布即写 trainings/<数据集名>.tflite,客户端固定下载,无存档回退)
label_task 标注任务 id(PK)、dataset_idfilenames(JSON 选中图片列表,NULL=全量)、status(running/done)、total/donecreated_atfinished_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"}
  • channelwechat | alipay
  • planIdday | week | month(需在 config.yml plans 节点存在,金额由服务端按配置锁定,客户端不可改价)

响应 data

{
  "orderId": "O20260822001",
  "payParams": {
    "partnerId": "1900xxxxx",
    "prepayId": "wx...",
    "nonceStr": "...",
    "timeStamp": "1728000000",
    "sign": "...",
    "packageValue": "Sign=WXPay"
  }
}
  • channel == wechatpayParams 为微信 APP 支付统一下单返回参数(后端签名)
  • channel == alipaypayParams{"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: falseexpiresAt 已过期(含未充值账号)→ 客户端展示付费墙/充值引导。

支付回调(微信/支付宝,勿直连)

渠道 路径 说明
微信支付(APP 支付 V3 POST /api/v1/payment/wechat/notify 解密+验签回调,out_trade_no → 落授权
支付宝(APP 支付) POST /api/v1/payment/alipay/notify RSA2 验签回调,out_trade_no → 落授权

GET /api/v1/app/update

App 版本更新检查(公开接口,无需 token,未登录/旧版本均可访问)。返回服务器最新版本记录;无任何记录时 data 为空对象,客户端视为无需更新。

响应 data

{
  "version": "1.1.0",
  "notes": "修复识别准确率问题"
}
  • 仅 Android 客户端调用(iOS 不做版本下发,走 App Store 自行更新)
  • 检测到新版本(服务器版本高于本地版本)即强制更新,客户端弹不可关闭的全屏提示,必须跳转更新后才能继续使用;本地已是新版本则不提示
  • 客户端以「语义化版本号」比较:1.10.0 > 1.9.9(按数字段比较,禁止字符串比较)
  • 下载地址为固定静态路径:/download/observer-latest.apkapp.apkDir 目录下永远只有最新一个文件,由后端静态托管),客户端拼 apiBaseUrl 访问
  • 模型热更新(与 APK 更新独立通道):服务器有已发布模型时响应额外返回 models 数组(与 GET /api/v1/models 同构:datasetId/datasetName/version/labels/sizeBytes/sha256/downloadUrl);客户端启动与 APK 更新独立检查——某数据集服务器版本高于本地已下载版本即下载 /download/trainings/<数据集名>.tflite 到应用私有目录,sha256 校验后原子替换,下次识别生效;非强制,失败回退旧模型下次启动重试。App 模型管理页列出服务器全部可用模型,用户自由下载/删除/启用;识别时加载全部已启用模型并行推理 + 跨模型 NMS 合并(按类别名),内置 assets 模型兜底。无发布模型时不返回 models 字段(旧 App 忽略新字段、新 App 兼容旧服务器)

GET /download-page

APK 下载引导页(静态页面,源码在 h5/index.html,由后端 /download-page 路径托管):微信内置浏览器会拦截 APK 下载,此页面按打开环境分流——微信内打开显示图形引导(点击右上角「···」→「在浏览器打开」+ 复制链接兜底);手机浏览器打开直接显示下载按钮,直链 http://observer.redpowerfuture.com/download/observer-latest.apk

管理端接口(/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 字)
GET /admin/app-versions 版本记录列表,page/size 分页,按下发时间倒序
POST /admin/app-versions 下发新版本(multipart/form-data):notes + fileAPK 文件,仅接受 .apk);版本号从文件名识别,文件须命名为 observer-x.y.z.apk(如 observer-1.0.1.apk),格式不符拒绝;版本号不可重复,APK 上传覆盖 app.apkDir/observer-latest.apk(目录永远只有一个文件);检测到新版本即强制更新
POST /admin/app-versions/delete 删除版本记录 {"id":1}:删最新版本时联动删除 APK 文件(客户端不再提示更新、下载 404);删历史版本只删记录不动文件
POST /admin/datasets 创建数据集 {"name":"pheasant_v2","namePrefix":"pheasant","source":"manual"|"ai"}(name ≤50 字唯一,目录自动建;namePrefix=AI 生成图文件名前缀,生成图按 <前缀>_<两位序号>.jpg 顺序命名)
GET /admin/datasets 数据集列表:page/size 分页,返回 {total, list}(含 imageCount/labeledCount/status/cover/description/training 聚合状态:最新训练记录的 status/currentEpoch/totalEpochs
POST /admin/datasets/update 更新数据集配置 {"id":1,"name":"新名","namePrefix":"pheasant","description":"...","cover":"a.jpg"}:名称(改名)/文件名前缀/描述/封面,空值字段不覆盖原值;改名同步迁移图片目录与模型文件,标注/训练进行中拒绝
POST /admin/datasets/images/vlm-review VLM 藏匿位补检 {"datasetId":1,"imageId":5}(两阶段标注第二阶段,须与图像生成显存互斥):qwen3.8-9b(+mmproj) 排除已确认框,按环境/光线/习性推理藏匿位,追加 ≤3 个疑似框(class 1)进同一 labels_json
POST /admin/datasets/cover 上传数据集封面(multipartdatasetId+filejpg/jpeg/png ≤2MB):自动转 jpg + UUID 命名落盘并覆盖旧封面
GET /admin/datasets/cover 封面文件(静态字节流,datasetId 定位)
POST /admin/datasets/cover/delete 删除数据集封面 {"datasetId":1}:删文件 + 清 cover 字段
POST /admin/datasets/upload 上传图片(multipart/form-datadatasetId + files 多张,仅接受 .jpg/.jpeg/.png),存 app.datasetDir/datasets/<name>/,逐张入库
POST /admin/datasets/generate AI 生成图片 {"datasetId":1,"species":"家鸽","count":1,"distance":25}(species 可留空=物种池随机;scene/action/light 后端随机;prompt 可手填覆盖模板;每张固定 1 个目标(数量词 "1只"animal_count=1 供标注裁剪);distance 必填注入提示词距离描述;距离校验已取消;文件名前缀取数据集属性)(prompt 留空时按 imageGen.promptTemplate 组装:物种按数据集名取 speciesByDataset 池、数量词=表单 animalCount、场景/动作/光线随机;手填覆盖)(count 1..8,同步执行,每张超时 imageGen.timeoutSeconds):调 imageGen providerdashscope / localaiconfig 切换)逐张生成落盘 + 入库(记录 prompt),任意失败返回错误;namePrefix 可选,按 <前缀>_<两位序号>.jpg 顺序命名并续接已有最大序号(如 pigeon_01.jpg),留空用时间戳命名;distance(米)与 animalCount 填写后逐张校验——RF-DETR 检测目标框,距离校验已取消(2026-08-28,无逐张拒检,生成即入库);animalCount 仅作为自动标注框数上限(按置信度裁剪 ≤N),不参与生成校验
GET /admin/datasets/images 数据集图片列表 {"datasetId":1},返回图片元数据(文件名/来源/prompt/创建时间)
GET /admin/datasets/image 图片文件(静态字节流,datasetId + filename 定位,供缩略图/查看)
POST /admin/datasets/images/delete 删除图片 {"datasetId":1,"ids":[1,2]}:删文件 + 删记录(AI 生成图是付费资产,前端确认文案提示)
GET /admin/datasets/export 导出数据集 zipdatasetId,打包图片目录为 zip 下载(标注衔接用)
POST /admin/datasets/sync 同步数据集到训练机 {"id":1}:按 training 通道推送图片到训练机 datasetDir/<name>/subprocess 同机 cp、ssh 异机 scp/rsync);训练启动前自动执行
POST /admin/label-tasks 发起预标注 {"datasetId":1,"filenames":["a.jpg","b.jpg"]}(详情页「全量标注」按钮入口,图片入库亦自动触发):filenames 缺省=全量,指定图则只扫描选中图(全量重标/单张修补);调 config.yml localAi 配置的 AI 端点 RF-DETR 逐张推理(common 池并行,未配置报错),扫描结果做重叠去重(NMS 风格按置信度降序保留,重叠比 > localAi.overlapThreshold 默认 0.3 的框剔除,同目标只留置信度最高者)后直写 dataset_image.labels_json(重跑覆盖该图标注)label_task 记录进度
GET /admin/label-tasks 标注任务列表:page/size 分页(含 status/total/done
GET /admin/label-tasks/detail 标注任务详情:返回数据集全部图片 + 每张标注框(boxesYOLO 归一化 xywh + 置信度 + 类别)
POST /admin/label-tasks/save 保存单张标注 {"datasetId":1,"filename":"a.jpg","boxes":[{"class":0,"cx":0.5,"cy":0.4,"w":0.1,"h":0.2}]}:整体覆写该图 labels_json(空 boxes=清空标注),返回该数据集当前 labeledCount
POST /admin/trainings 发起训练 {"dataset":"yolo","imgsz":704,"epochs":150,"batch":16,"device":"0","name":"..."}:先同步数据集到训练机 → 校验目录存在 → runner 启动训练;并发度 1,已有 running 任务时返回错误
GET /admin/trainings 训练任务列表:page/size 分页,按下发时间倒序,含 status/进度/指标
GET /admin/trainings/detail 任务详情 {"id":1}:参数快照 + 进度 + 指标 + 日志尾部
POST /admin/trainings/cancel 取消训练 {"id":1}(仅 running):杀训练进程,状态置 failed(记录 error
POST /admin/trainings/publish 发布为最新模型 {"id":1,"notes":"..."}(仅 success):tflite 拷为 workspace/model-latest.tflite(原子覆盖)+ sha256/大小 → 插入 model_version(版本号递增 m1.0.0 → m1.0.1+ 旧版 is_latest=0
GET /admin/label-workbench 标注工作台数据 {"datasetId":1}:数据集全部图片 + 每张标注框(boxeslabels_json 全量)——无历史标注任务时详情页工作台的数据源

管理页面由 server_admin/ 构建产物提供,访问 http://<host>/admin/。金额均为整数分,前端展示 ÷100 转元。

GET /api/v1/models

模型目录(需 Bearer token,客户端模型管理页拉取):返回全部数据集当前生效模型。响应 data

{
  "list": [
    {"datasetId": 1, "datasetName": "pheasant", "version": "m1.2.0", "labels": ["pheasant", "suspect"],
     "sizeBytes": 6400000, "sha256": "ab12...", "notes": "修复小目标漏检", "publishedAt": "2026-08-26T10:00:00+08:00",
     "downloadUrl": "/download/models/pheasant/latest.tflite"}
  ]
}
  • 只返回 is_latest=1 的模型(每数据集至多一条);无任何发布模型时 list 为空数组
  • 下载地址由客户端拼 apiBaseUrl 访问;下载文件 sha256 校验,类别名数组 labels 用于多模型合并推理展示

使用说明

  1. 配置 config.yml:监听端口、数据库路径、登录 token 签名密钥 auth.secret(必填,换值即全员下线)、套餐 plans 节点、微信支付(appid/mchid/商户私钥/证书序列号/APIv3 密钥)、支付宝(appid/应用私钥/支付宝公钥)、管理端 admin.token;模型训练相关节点:training(训练通道 mode=subprocess/ssh、ssh 连接信息、训练机工作目录/venv/数据集目录、并发度 1、超时)、imageGenAI 生成图片 providerdashscope 通义万相(apiKey + model qwen-image-3.0/ localai 本地 local-aibaseUrl + model 如 qwen-image + timeoutSeconds,训练机约 5 分钟/张建议 600))、localAi(二期标注用 RF-DETR 服务地址);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 devVite 代理 /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.dartapiBaseUrl、微信/支付宝 AppID 与 URL Scheme 替换为真实值

金额与安全约定

  • 金额一律整数分price_cents / amount_cents 为 INT64),禁止浮点元;前端展示 ÷100
  • 支付回调必须验签(微信 APIv3 验签/解密、支付宝 RSA2),回调处理幂等wx_trade_no/alipay_trade_no 唯一约束,重复回调直接忽略)
  • 授权落库以较晚到期为准:已有授权未过期时新套餐顺延叠加,禁止覆盖缩短