初始化天卡开放平台后端项目

包含完整功能模块:
- 设备激活(直销激活码模式 + 代销免激活码模式)
- 微信支付 + 天卡订单
- 天卡池管理 + 异步橘视厅SDK激活
- 第三方合作伙伴接口(代销模式)
  - 签名鉴权(appKey/appSecret + nonce防重放)
  - 天卡消耗(事务 + 幂等 + 并发控制)
  - 媒资接口(三级授权过滤)
  - 审计日志
- 定价策略(3层:门店 > 渠道 > 平台)
- 分账(微信分账 + 分润规则)
- Docker部署(含Redis容器)
This commit is contained in:
zhaogd
2026-07-26 12:34:13 +08:00
commit 30198c7191
349 changed files with 28837 additions and 0 deletions
@@ -0,0 +1,138 @@
package io.as.modules.openapi.sign.impl;
import io.as.common.utils.RedisUtils;
import io.as.modules.openapi.sign.ApiDeviceSignService;
import io.as.modules.openapi.sign.DeviceSignCache;
import io.as.modules.openapi.sign.DeviceSignResult;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* 设备 sign:HMAC-SHA256(deviceId:timestamp:orgCode, secret),Base64URL 输出;校验时验证签名并检查时间戳在有效期内
* <p>对齐生产环境 as-partner-backend 的 ApiDeviceSignServiceImpl</p>
*/
@Service
public class ApiDeviceSignServiceImpl implements ApiDeviceSignService {
private static final String HMAC_SHA256 = "HmacSHA256";
/** sign 有效时长(毫秒):24 小时 */
private static final long SIGN_VALID_MS = 24 * 60 * 60 * 1000;
/** Redis key 前缀:openapi: 隔离于生产 api:jst: */
private static final String REDIS_KEY_LATEST_SIGN_PREFIX = "openapi:device-sign:";
@Value("${renren.api.device-sign-secret:renren-device-sign-secret-change-in-prod}")
private String secret;
@Autowired
private RedisUtils redisUtils;
@Override
public String generate(Long deviceId, long timestampMs, String orgCode) {
String orgCodePart = orgCode == null ? "" : orgCode.trim();
String payload = deviceId + ":" + timestampMs + ":" + orgCodePart;
String hmac = hmacSha256Hex(payload, secret);
String toEncode = payload + ":" + hmac;
String sign = Base64.getUrlEncoder().withoutPadding().encodeToString(toEncode.getBytes(StandardCharsets.UTF_8));
if (deviceId != null) {
String key = REDIS_KEY_LATEST_SIGN_PREFIX + deviceId;
DeviceSignCache cache = new DeviceSignCache(deviceId, orgCodePart, sign, timestampMs);
redisUtils.set(key, cache, SIGN_VALID_MS / 1000);
}
return sign;
}
@Override
public DeviceSignResult verify(String sign) {
if (StringUtils.isBlank(sign)) {
return null;
}
try {
String decoded = new String(Base64.getUrlDecoder().decode(sign.trim()), StandardCharsets.UTF_8);
int lastColon = decoded.lastIndexOf(':');
if (lastColon <= 0) {
return null;
}
String payload = decoded.substring(0, lastColon);
String hmacReceived = decoded.substring(lastColon + 1);
String hmacExpected = hmacSha256Hex(payload, secret);
if (!hmacExpected.equalsIgnoreCase(hmacReceived)) {
return null;
}
// payload: deviceId:timestamp:orgCode
String[] parts = payload.split(":", 3);
if (parts.length < 2) {
return null;
}
Long deviceId = Long.parseLong(parts[0].trim());
long timestamp = Long.parseLong(parts[1].trim());
String orgCode = parts.length >= 3 ? parts[2].trim() : null;
long now = System.currentTimeMillis();
if (timestamp > now || (now - timestamp) > SIGN_VALID_MS) {
return null;
}
if (deviceId == null) {
return null;
}
// 结合 Redis 校验:只允许当前设备最新 sign 生效(若 Redis 有记录且不同,则判为旧 sign)
String key = REDIS_KEY_LATEST_SIGN_PREFIX + deviceId;
DeviceSignCache cache = redisUtils.get(key, DeviceSignCache.class);
if (cache != null && !sign.equals(cache.getSign())) {
return null;
}
// Redis 没记录时,用签名中自带的 orgCode;有记录时以缓存中的为准
if (cache != null && cache.getOrgCode() != null) {
orgCode = cache.getOrgCode();
}
return new DeviceSignResult(deviceId, timestamp, orgCode);
} catch (Exception e) {
return null;
}
}
@Override
public void refreshExpire(Long deviceId) {
if (deviceId == null) {
return;
}
String key = REDIS_KEY_LATEST_SIGN_PREFIX + deviceId;
DeviceSignCache cache = redisUtils.get(key, DeviceSignCache.class);
if (cache != null) {
redisUtils.set(key, cache, SIGN_VALID_MS / 1000);
}
}
@Override
public void invalidate(Long deviceId) {
if (deviceId == null) {
return;
}
String key = REDIS_KEY_LATEST_SIGN_PREFIX + deviceId;
// 哨兵缓存:verify 时 cache != null 且 sign != "REVOKED" 即拒绝旧 sign,
// 直到重新 init 调 generate 覆盖此哨兵或 TTL 过期。
DeviceSignCache revoked = new DeviceSignCache(deviceId, null, "REVOKED", System.currentTimeMillis());
redisUtils.set(key, revoked, SIGN_VALID_MS / 1000);
}
private static String hmacSha256Hex(String data, String secretKey) {
try {
Mac mac = Mac.getInstance(HMAC_SHA256);
mac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), HMAC_SHA256));
byte[] hmac = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder(hmac.length * 2);
for (byte b : hmac) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (Exception e) {
throw new RuntimeException("HMAC error", e);
}
}
}