记得上下班打卡 | git大法好,push需谨慎

Commit 70d5c233 authored by 姜秀龙's avatar 姜秀龙

Merge branch 'jxl-datongfensi' into test-ecs

parents 892ac840 60674a7b
# 粉丝俱乐部 ↔ 正在现场 技术设计(已定稿)
服务前缀:`/adam``/order``/kylin``/goblin`
**已定**:多端 JTI · 静默登录 `silent_mobile` · 订单来源标记 · A 侧列表用 B token 调**原接口**、不过滤来源(不新建列表接口)
---
## 一、多端登录(JTI)
| 模块 | 改动 |
|------|------|
| **adam** `AdamLoginController#ssoProcess`、各 `login/*` | JWT 加 `jti`,Redis `session:{jti}`;去掉 `adam:identity:sso:{uid}` 互踢 |
| **adam** `AdamAuthorityInterceptor`、各业务拦截器 | 验 JWT 后查 `jti` 是否有效 |
---
## 二、静默登录
**链路**`A 前端 → A 后端 → POST /adam/login/silent_mobile`(A 前端不调 B)
| 接口 | 说明 |
|------|------|
| `POST /adam/login/silent_mobile` | `otp = DES(手机号11位+Unix秒)`,密钥仅 A/Adam 服务端配置化 |
| A 自建如 `POST /api/launch-zhengzai` | 代调 silent_mobile,把 **B token** 给 WebView / 前端 |
A 侧订单:持 **B token** 调现有 `GET /kylin/order/list``POST /goblin/order/list` 等,**不加来源筛选**
---
## 三、订单来源标记
### 入参(`PayOrderParam` / `GoblinOrderParam`)
| 字段 | 落库 |
|------|------|
| `referrerApp` | ✅ → 演出/商品均为 `order_source`**与 referrerUserId 同时传**才写入,否则 `''` |
| `referrerUserId` | ✅ → `referrer_user_id`;未传写 `''` |
### 落库与实现位置
| 表 | 粉丝俱乐部来源 | 客户端来源(不变) | 新增 |
|----|----------------|-------------------|------|
| `kylin_order_tickets` | `order_source` | `order_type`(header) | `referrer_user_id` |
| `goblin_store_order` | `order_source` | `source`(header) | `referrer_user_id` |
`POST /order/pre``POST /goblin/pre``KylinOrderTicketsServiceImpl#order``GoblinOrderServiceImpl` 建单处赋值。
### 列表 / 详情返回
| 类型 | VO |
|------|-----|
| 演出 | `KylinOrderListVo``KylinOrderTicketVo`(详情内)字段 `referrerApp``referrerUserId` |
| 商品 | `GoblinAppOrderListVo``GoblinStoreOrderVo`(详情内)字段 `referrerApp``referrerUserId` |
DDL:`docu/order_referrer_kylin.sql``docu/order_referrer_goblin.sql`
---
## 四、分批实现(建议每批一个 commit)
| 顺序 | 范围 | 建议 commit message |
|------|------|---------------------|
| 1 | JTI 多端登录(adam 签发 + Redis 会话 + 拦截器去掉互踢) | `feat(adam): 多端登录 JTI 会话,取消单设备互踢` |
| 2 | `silent_mobile` 密钥配置化(若需)+ 文档/注释 | `chore(adam): silent_mobile 密钥配置化`(可选,与 1 合并亦可) |
| 3 | 演出订单 DDL + 落库 + 列表详情 VO | `feat(kylin): 演出订单粉丝俱乐部来源标记` |
| 4 | 商品订单 DDL + 落库 + 列表详情 VO | `feat(goblin): 商品订单粉丝俱乐部来源标记` |
> A 项目(粉丝俱乐部)侧:`launch-zhengzai`、WebView 传参、持 B token 调原列表接口 — **在 A 仓库单独 commit**,不在本仓库。
---
## 变更记录
| 日期 | 说明 |
|------|------|
| 2026-05-16 | 定稿;移除新建列表接口;A 用 B token 调原接口 |
......@@ -15,7 +15,6 @@ liquidnet:
adam:
# 静默登录 v2,与粉丝俱乐部 A 后端同配,生产请在配置中心设置
silent-otp-v2-secret: 4bG9EhEd3gtd51lO9dbHHbVy7G7QffRs
silent-otp-v2-window-seconds: 10
mysql:
urlHostAndPort: java-test.mysql.polardb.rds.aliyuncs.com:3306
username: zhengzai
......
......@@ -14,7 +14,6 @@ liquidnet:
blacklist_grace_period: 5
adam:
silent-otp-v2-secret: 4bG9EhEd3gtd51lO9dbHHbVy7G7QffRs
silent-otp-v2-window-seconds: 10
mysql:
urlHostAndPort: java-test.mysql.polardb.rds.aliyuncs.com:3306
username: zhengzai
......
......@@ -8,10 +8,9 @@ import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
/**
* 静默登录 v2:otp = hex(HMAC-SHA256(secret, mobile + Unix秒UTC)),与 v1 DES 隔离
* 静默登录 v2:otp = hex(HMAC-SHA256(secret, mobile)),与 v1 DES 隔离。A/B 两侧均不带时间戳
*/
@Component
public class SilentMobileOtpV2Support {
......@@ -21,9 +20,6 @@ public class SilentMobileOtpV2Support {
@Value("${liquidnet.adam.silent-otp-v2-secret:}")
private String secret;
@Value("${liquidnet.adam.silent-otp-v2-window-seconds:10}")
private int windowSeconds;
public boolean verify(String mobile, String otp) {
if (StringUtils.isBlank(secret) || StringUtils.isBlank(mobile) || StringUtils.isBlank(otp)) {
return false;
......@@ -31,35 +27,24 @@ public class SilentMobileOtpV2Support {
if (!otp.matches("^[0-9a-fA-F]{64}$")) {
return false;
}
long now = Instant.now().getEpochSecond();
long from = now - windowSeconds;
if (from < 0) {
from = 0;
}
byte[] otpBytes = hexToBytes(otp);
if (otpBytes == null) {
return false;
}
for (long ts = from; ts <= now; ts++) {
byte[] expected = sign(mobile, ts);
if (expected != null && MessageDigest.isEqual(expected, otpBytes)) {
return true;
}
}
return false;
byte[] expected = sign(mobile);
return expected != null && MessageDigest.isEqual(expected, otpBytes);
}
public String signHex(String mobile, long epochSecondUtc) {
byte[] raw = sign(mobile, epochSecondUtc);
public String signHex(String mobile) {
byte[] raw = sign(mobile);
return raw == null ? null : bytesToHex(raw);
}
private byte[] sign(String mobile, long epochSecondUtc) {
private byte[] sign(String mobile) {
try {
String payload = mobile + epochSecondUtc;
Mac mac = Mac.getInstance(HMAC_SHA256);
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_SHA256));
return mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
return mac.doFinal(mobile.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
return null;
}
......
......@@ -116,7 +116,6 @@ public class TestAdam {
@Test
public void testLoginBySilentMobileV2() {
String mobile = "15811009011";
long ts = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
String secret = System.getenv("LIQUIDNET_ADAM_SILENT_OTP_V2_SECRET");
if (secret == null || secret.isEmpty()) {
System.out.println("skip v2 test: set env LIQUIDNET_ADAM_SILENT_OTP_V2_SECRET");
......@@ -124,7 +123,7 @@ public class TestAdam {
}
javax.crypto.Mac mac = javax.crypto.Mac.getInstance("HmacSHA256");
mac.init(new javax.crypto.spec.SecretKeySpec(secret.getBytes(java.nio.charset.StandardCharsets.UTF_8), "HmacSHA256"));
byte[] raw = mac.doFinal((mobile + ts).getBytes(java.nio.charset.StandardCharsets.UTF_8));
byte[] raw = mac.doFinal(mobile.getBytes(java.nio.charset.StandardCharsets.UTF_8));
StringBuilder otp = new StringBuilder();
for (byte b : raw) {
otp.append(String.format("%02x", b & 0xff));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment