转到文件
XuqmGroup 8de0338b93 fix: withdraw previously-approved store reviews before re-submitting same version
When re-submitting a version to stores where the previous review was APPROVED:

1. markSubmitted now sets those stores to WITHDRAWN (not PENDING), preserving
   the old batchId/submittedAt for traceability. This signals to executeSubmitAsync
   that the store cancel API must be called before a new submission is attempted.

2. executeSubmitAsync detects the WITHDRAWN state and calls cancelAtStore first,
   then falls through to the normal submission path. This revokes the old approval
   on the store's side so no stale webhook or poll cycle can fire APPROVED for the
   old review after re-submission.

3. updateStoreReview now rejects APPROVED transitions from PENDING or WITHDRAWN
   states (stale webhook guard). A valid approval can only arrive after the store
   has seen the new submission (i.e. current state must be SUBMITTING or UNDER_REVIEW).
   This prevents autoPublishAfterReview from triggering before the new review cycle.

Operation log includes `approvedWithdrawn` list when any store was withdrawn on re-submit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 17:09:31 +08:00
common docs(deploy): 添加完整的部署文档和配置示例 2026-05-09 14:53:42 +08:00
demo-service 一大波改动 2026-05-15 16:47:22 +08:00
docs docs(deploy): 添加完整的部署文档和配置示例 2026-05-09 14:53:42 +08:00
file-service fix(file-service): stream upload to disk to fix OOM on large files 2026-05-18 16:31:15 +08:00
im-sdk feat: remove Update methods from Java SDK, keep only IM and Push 2026-05-16 12:10:07 +08:00
im-service fix: return 401 (not 403) for unauthenticated requests across all services 2026-05-18 13:31:24 +08:00
license-service fix: return 401 (not 403) for unauthenticated requests across all services 2026-05-18 13:31:24 +08:00
push-service fix: return 401 (not 403) for unauthenticated requests across all services 2026-05-18 13:31:24 +08:00
tenant-service feat: webhook shows app name; auto-withdraw superseded approved stores 2026-05-18 13:31:31 +08:00
update-service fix: withdraw previously-approved store reviews before re-submitting same version 2026-05-18 17:09:31 +08:00
.dockerignore feat(im): 添加平台事件通知功能支持应用审核状态实时更新 2026-05-08 18:32:46 +08:00
.gitignore chore: initial commit 2026-04-21 22:07:29 +08:00
.java-version docs(deploy): 添加部署文档和安全设计规范 2026-05-08 18:32:00 +08:00
Dockerfile perf: Maven BuildKit cache mount + pom-first layering + Alpine JRE; ci: Jenkinsfile固定main分支 2026-05-16 11:33:25 +08:00
Jenkinsfile perf: Maven BuildKit cache mount + pom-first layering + Alpine JRE; ci: Jenkinsfile固定main分支 2026-05-16 11:33:25 +08:00
maven-settings.xml feat(im): 添加平台事件通知功能支持应用审核状态实时更新 2026-05-08 18:32:46 +08:00
pom.xml Add license service and tenant integration 2026-05-15 21:00:24 +08:00
README.md docs(deploy): 添加完整的部署文档和配置示例 2026-05-09 14:53:42 +08:00

XuqmGroup-Server 后端文档

当前推荐阅读入口:/docs/server/README.md
仓库内联调文档:docs/API_ACCESS.md
该文档保留为仓库内说明,线上地址、初始化账号与最新联调接口请以最新文档为准。

Spring Boot 3.4.4 · Java 21 · Maven 多模块

模块结构

XuqmGroup-Server/
├── pom.xml                  # 父 POM,统一依赖版本
├── common/                  # 公共库ApiResponse、JWT、异常处理
├── tenant-service/          # 租户服务      :8081
├── im-service/              # IM 服务       :8082
├── push-service/            # 推送服务      :8083
└── update-service/          # 版本管理服务  :8084

技术栈

组件 版本
Spring Boot 3.4.4
Java 21
Spring Security 6 JWT 无状态认证
Spring Data JPA Hibernate 6,ddl-auto: update
MySQL 8.x
Redis 7.x验证码、子账号会话
JJWT 0.12.6,HMAC-SHA256
Spring Mail 邮件验证码
Spring WebSocket + STOMP IM 实时通信

快速启动

# 前置MySQL 8 + Redis 7 本地运行
cd XuqmGroup-Server

# 启动所有服务(各模块独立启动)
cd tenant-service && mvn spring-boot:run &
cd im-service     && mvn spring-boot:run &
cd push-service   && mvn spring-boot:run &
cd update-service && mvn spring-boot:run &

首次启动会自动建表ddl-auto: update
运营平台默认管理员:admin / Admin@123456(可通过 ops.admin.* 配置覆盖)。


tenant-service租户服务:8081

数据库表

表名 说明
t_tenant 租户主账号 & 子账号
t_app 应用
t_feature_service 功能服务IM/推送/版本管理)
t_email_verification 邮箱验证码
t_ops_admin 运营平台管理员

接口列表

认证(无需 Token

方法 路径 说明
GET /api/auth/captcha 获取图形验证码
POST /api/auth/send-email-code?email=&purpose= 发送邮箱验证码purpose: REGISTER / FORGOT_PASSWORD / SUB_ACCOUNT
POST /api/auth/register 注册主账号
POST /api/auth/login 登录,返回 JWT
POST /api/auth/forgot-password?email= 发送重置密码邮件
POST /api/auth/reset-password?email=&code=&newPassword= 重置密码

注册请求体

{
  "username": "demo",
  "password": "Demo@123456",
  "email": "demo@example.com",
  "nickname": "Demo",
  "emailCode": "AB3K"
}

登录请求体

{
  "account": "demo",
  "password": "Demo@123456",
  "captchaKey": "uuid-from-captcha-api",
  "captchaCode": "XK2P"
}

登录响应

{
  "code": 200, "status": "0",
  "data": { "token": "eyJ..." },
  "message": "success"
}

应用管理(需 Token

方法 路径 说明
GET /api/apps 查询当前租户的应用列表
GET /api/apps/{id} 查询应用详情
POST /api/apps 创建应用
PUT /api/apps/{id} 更新应用信息
DELETE /api/apps/{id} 删除应用

创建应用请求体

{
  "name": "我的App",
  "packageName": "com.example.myapp",
  "description": "描述",
  "iconUrl": "https://cdn.example.com/icon.png"
}

应用对象(响应)

{
  "id": "uuid",
  "tenantId": "uuid",
  "name": "我的App",
  "packageName": "com.example.myapp",
  "appKey": "ak_xxxxxxxx",
  "appSecret": "as_xxxxxxxx",
  "createdAt": "2026-04-21T00:00:00"
}

说明SDK 和 demo 侧统一传 appKey。当前默认值是 ak_demo_chat,如果数据库里没有这条记录,tenant-service 会在启动时自动创建。IM 登录必须先存在注册用户,不再支持“登录即注册”。

功能服务(需 Token

方法 路径 说明
GET /api/apps/{appKey}/services 获取应用下所有功能服务
POST /api/apps/{appKey}/services/toggle?platform=&serviceType=&enable= 开启/关闭功能服务
POST /api/apps/{appKey}/services/{id}/regenerate-key 重新生成 secretKey

platform 枚举ANDROID / IOS / HARMONY
serviceType 枚举IM / PUSH / UPDATE

子账号(需 Token

方法 路径 说明
GET /api/sub-accounts 查询子账号列表
POST /api/sub-accounts/send-verify-code?email= 发送邮箱验证码24h 内有效一次)
POST /api/sub-accounts/verify-email?email=&code= 验证邮箱(通过后 24h 内可创建)
POST /api/sub-accounts 创建子账号(需先验证邮箱)
DELETE /api/sub-accounts/{id} 禁用子账号
GET /api/sub-accounts/generate-password 生成随机密码

创建子账号请求体

{
  "username": "sub_user",
  "password": "Sub@123456",
  "email": "sub@example.com",
  "nickname": "子账号"
}

运营平台(需 OPS Token

方法 路径 说明
POST /api/auth/ops/login 运营平台登录
GET /api/ops/tenants?keyword=&page=&size= 分页查询租户
POST /api/ops/tenants/{id}/toggle-status 启用/禁用租户
GET /api/ops/statistics 获取统计数据

运营登录请求体

{ "username": "admin", "password": "Admin@123456" }

统计响应

{
  "data": {
    "totalTenants": 128,
    "todayNew": 5,
    "activeApps": 42,
    "onlineUsers": 0
  }
}

im-serviceIM 服务):8082

数据库表

表名 说明
im_account IM 用户账号(仅存 userId,不含昵称/头像)
im_group 群组memberIds、adminIds 为逗号分隔字符串)
im_message 消息记录
im_keyword_filter 关键词过滤规则
im_webhook_config Webhook 配置

IM 账号登录

POST /api/im/auth/login
  ?appKey=ak_xxx
  &userId=user_001
  &userSig=your_user_sig

服务端可本地签发 userSig,IM 管理页也支持生成与校验;管理员账号可用于服务端 SDK / REST API。

响应:{ "data": { "token": "eyJ..." } }

HTTP 消息接口

方法 路径 说明
POST /api/im/messages/send?appKey= 发送消息
POST /api/im/messages/{id}/revoke?appKey= 撤回消息
GET /api/im/messages/history/{toId}?appKey=&page=&size= 查询历史消息

发送消息请求体

{
  "toId": "user_002",
  "chatType": "SINGLE",
  "msgType": "TEXT",
  "content": "Hello!",
  "mentionedUserIds": ""
}

消息类型MsgType

说明
TEXT 文本(支持表情 Unicode / 自定义表情 ID
IMAGE 图片content 为 URL
VIDEO 视频
AUDIO 语音
FILE 文件
CUSTOM 自定义content 为 JSON 字符串)
LOCATION 位置content 为 {"lat":x,"lng":y,"address":"..."})
NOTIFY 系统通知
RICH_TEXT 图文混排
CALL_AUDIO 音频通话信令
CALL_VIDEO 视频通话信令
FORWARD 转发content 为原消息 JSON
REVOKED 已撤回(系统占位,不可主动发送)

WebSocket 实时通信

连接

ws://localhost:8082/ws/im?token=<im_jwt>

支持 STOMP 协议(可直接使用原生 WebSocket 发 JSON frame

发送消息(客户端 → 服务端)

{
  "destination": "/app/chat.send",
  "payload": {
    "appKey": "ak_xxx",
    "toId": "user_002",
    "chatType": "SINGLE",
    "msgType": "TEXT",
    "content": "Hello!"
  }
}

接收消息(服务端 → 客户端)

  • 单聊推送至:/user/{userId}/queue/messages
  • 群聊推送至:/topic/group/{groupId}

Frame 格式:

{
  "type": "MESSAGE",
  "payload": {
    "id": "uuid",
    "fromId": "user_001",
    "toId": "user_002",
    "chatType": "SINGLE",
    "msgType": "TEXT",
    "content": "Hello!",
    "revoked": false,
    "createdAt": "2026-04-21T10:00:00"
  }
}

撤回消息(客户端 → 服务端)

{
  "destination": "/app/chat.revoke",
  "payload": { "appKey": "ak_xxx", "messageId": "msg-uuid" }
}

撤回推送(服务端 → 客户端)

{
  "type": "REVOKE",
  "payload": { "msgId": "msg-uuid", "operatorId": "user_001" }
}

关键词过滤

im_keyword_filter 表中配置,支持 REPLACE(替换为 *)和 BLOCK拒绝发送两种动作,pattern 为正则表达式。

Webhook

im_webhook_config 表中为 App 配置 URL,消息发送后异步 HTTP POST 通知,超时 3000ms,格式

{
  "event": "message",
  "appKey": "ak_xxx",
  "message": { ...消息对象... }
}

push-service推送服务:8083

数据库表

表名 说明
push_device_token 设备推送 tokenappKey + userId + vendor 唯一)

支持厂商

HUAWEI / XIAOMI / OPPO / VIVO / HONOR / APNSiOS

接口

方法 路径 说明
POST /api/push/register 注册设备 token
POST /api/push/receive-push 开启或关闭接收推送
DELETE /api/push/device/unregister 解绑设备 token
POST /api/push/send 向指定用户推送通知
POST /api/push/internal/notify IM 服务内部调用,批量触发离线推送

其中 /api/push/register/api/push/device/unregister/api/push/receive-push 走 JWT 鉴权;/api/push/internal/notify 走内部 token。

注册 token

POST /api/push/register
  ?appKey=ak_xxx
  &userId=user_001
  &vendor=HUAWEI
  &token=device_push_token

发送推送

POST /api/push/send
  ?appKey=ak_xxx
  &userId=user_001
  &title=新消息
  &body=张三: Hello!
  &payload={"type":"IM","msgId":"uuid"}

开关接收推送

POST /api/push/receive-push
  ?appKey=ak_xxx
  &userId=user_001
  &enabled=false

内部通知

{
  "appKey": "ak_xxx",
  "userIds": ["user_001", "user_002"],
  "title": "群聊消息",
  "body": "张三: Hello!",
  "payload": "{\"type\":\"IM\",\"msgId\":\"uuid\"}"
}

环境变量配置

push:
  huawei:
    app-id: ${HUAWEI_APP_ID}
    app-secret: ${HUAWEI_APP_SECRET}
  xiaomi:
    app-secret: ${XIAOMI_APP_SECRET}
  apns:
    key-id: ${APNS_KEY_ID}
    team-id: ${APNS_TEAM_ID}
    key-path: ${APNS_KEY_PATH}      # .p8 文件路径
    bundle-id: ${APNS_BUNDLE_ID}
    production: false               # true = 生产环境

update-service版本管理服务:8084

数据库表

表名 说明
app_version 原生 App 版本Android APK / iOS 跳转链接)
rn_bundle RN Bundle 版本

App 版本管理

方法 路径 说明
GET /api/v1/updates/app/check 检查更新
POST /api/v1/updates/app/upload 上传版本(先传 file-service,再把 apkUrl 交给 update-service
POST /api/v1/updates/app/{id}/publish 发布版本
GET /api/v1/updates/app/list 版本列表
POST /api/v1/updates/store/app/{id}/execute-submit 批量提交应用市场,过程会写入批次日志与逐市场状态

检查更新

GET /api/v1/updates/app/check
  ?appKey=ak_xxx
  &platform=ANDROID
  &currentVersionCode=10

响应(有更新):

{
  "data": {
    "needsUpdate": true,
    "versionName": "1.1.0",
    "versionCode": 11,
    "downloadUrl": "http://update.xuqm.com/files/apk/xxx.apk",
    "changeLog": "修复若干问题",
    "forceUpdate": false,
    "appStoreUrl": "",
    "marketUrl": ""
  }
}

platform 枚举ANDROID / IOS / HARMONY

市场提审状态PENDING / SUBMITTING / UNDER_REVIEW / APPROVED / REJECTED

上传 APK两段式

POST /api/file/upload
  file=<binary>

拿到返回的 url 后,再调用:

POST /api/v1/updates/app/upload
  appKey=ak_xxx
  platform=ANDROID
  versionName=1.1.0
  versionCode=11
  changeLog=更新内容
  forceUpdate=false
  apkUrl=https://file.dev.xuqinmin.com/api/file/<hash>

RN Bundle 管理

方法 路径 说明
GET /api/v1/rn/update/check 检查 RN Bundle 更新
POST /api/v1/rn/upload 上传 Bundle 文件
POST /api/v1/rn/{id}/publish 发布 Bundle

检查 RN 更新

GET /api/v1/rn/update/check
  ?appKey=ak_xxx
  &moduleId=main
  &platform=android
  &currentVersion=1.0.0

响应(有更新):

{
  "data": {
    "needsUpdate": true,
    "latestVersion": "1.1.0",
    "downloadUrl": "http://...",
    "md5": "abc123...",
    "minCommonVersion": "1.0.0",
    "note": "热更新说明"
  }
}

上传 Bundlemultipart/form-data

POST /api/v1/rn/upload
  appKey=ak_xxx
  moduleId=main
  platform=ANDROID
  version=1.1.0
  minCommonVersion=1.0.0
  note=修复闪退
  bundle=<binary .bundle 文件>

服务端自动计算 MD5,存储至 {upload-dir}/rn/{appKey}/{platform}/{moduleId}/

环境变量配置

update:
  upload-dir: ${UPDATE_UPLOAD_DIR:/tmp/xuqm-update}
  base-url: ${UPDATE_BASE_URL:http://localhost:8084}