初始化天卡开放平台后端项目
包含完整功能模块: - 设备激活(直销激活码模式 + 代销免激活码模式) - 微信支付 + 天卡订单 - 天卡池管理 + 异步橘视厅SDK激活 - 第三方合作伙伴接口(代销模式) - 签名鉴权(appKey/appSecret + nonce防重放) - 天卡消耗(事务 + 幂等 + 并发控制) - 媒资接口(三级授权过滤) - 审计日志 - 定价策略(3层:门店 > 渠道 > 平台) - 分账(微信分账 + 分润规则) - Docker部署(含Redis容器)
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
package io.as.modules.openapi.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.modules.openapi.entity.ContentPackageEntity;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 开放API内容包授权
|
||||
*/
|
||||
public interface ContentPackageService extends IService<ContentPackageEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询内容包列表
|
||||
*
|
||||
* @param params 查询参数
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageUtils queryPage(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 根据合作伙伴ID获取生效的内容包
|
||||
*
|
||||
* @param partnerId 合作伙伴ID
|
||||
* @return 内容包实体,不存在返回 null
|
||||
*/
|
||||
ContentPackageEntity getByPartnerId(String partnerId);
|
||||
|
||||
/**
|
||||
* 保存或更新内容包(按 partnerId 判断)
|
||||
*
|
||||
* @param entity 内容包实体
|
||||
*/
|
||||
void saveOrUpdatePackage(ContentPackageEntity entity);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package io.as.modules.openapi.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.modules.openapi.entity.OpenApiCallLogEntity;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 开放API调用日志
|
||||
*/
|
||||
public interface OpenApiCallLogService extends IService<OpenApiCallLogEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询调用日志
|
||||
*
|
||||
* @param params 查询参数
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageUtils queryPage(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 保存调用日志
|
||||
*
|
||||
* @param partnerId 合作伙伴ID
|
||||
* @param appKey 应用Key
|
||||
* @param api 调用API
|
||||
* @param method HTTP方法
|
||||
* @param params 请求参数
|
||||
* @param resultCode 结果码
|
||||
* @param ip 调用方IP
|
||||
* @param costMs 耗时(毫秒)
|
||||
*/
|
||||
void saveLog(String partnerId, String appKey, String api, String method, String params, Integer resultCode, String ip, Integer costMs);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package io.as.modules.openapi.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.modules.openapi.entity.OpenApiPartnerEntity;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 开放API合作伙伴
|
||||
*/
|
||||
public interface OpenApiPartnerService extends IService<OpenApiPartnerEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询合作伙伴列表
|
||||
*
|
||||
* @param params 查询参数
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageUtils queryPage(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 根据 appKey 获取合作伙伴
|
||||
*
|
||||
* @param appKey 应用Key
|
||||
* @return 合作伙伴实体,不存在返回 null
|
||||
*/
|
||||
OpenApiPartnerEntity getByAppKey(String appKey);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package io.as.modules.openapi.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.modules.openapi.entity.PartnerCardPoolEntity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 合作伙伴天卡池
|
||||
*/
|
||||
public interface PartnerCardPoolService extends IService<PartnerCardPoolEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询天卡池列表
|
||||
*
|
||||
* @param params 查询参数
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageUtils queryPage(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 查询合作伙伴可用天卡数量
|
||||
*
|
||||
* @param partnerId 合作伙伴ID
|
||||
* @return 可用天卡数量
|
||||
*/
|
||||
int getAvailableCount(String partnerId);
|
||||
|
||||
/**
|
||||
* 获取合作伙伴可用天卡列表
|
||||
*
|
||||
* @param partnerId 合作伙伴ID
|
||||
* @param limit 取卡数量
|
||||
* @return 可用天卡列表
|
||||
*/
|
||||
List<PartnerCardPoolEntity> getAvailableCards(String partnerId, int limit);
|
||||
|
||||
/**
|
||||
* 根据天卡池ID查询
|
||||
*
|
||||
* @param poolId 天卡池ID
|
||||
* @return 天卡池实体,不存在返回 null
|
||||
*/
|
||||
PartnerCardPoolEntity getByPoolId(Long poolId);
|
||||
|
||||
/**
|
||||
* 批量保存采购的天卡
|
||||
*
|
||||
* @param partnerId 合作伙伴ID
|
||||
* @param purchaseBatch 采购批次
|
||||
* @param cards 天卡列表
|
||||
*/
|
||||
void saveBatchCards(String partnerId, String purchaseBatch, List<PartnerCardPoolEntity> cards);
|
||||
|
||||
/**
|
||||
* 采购天卡:从平台天卡池划拨指定数量到合作伙伴池子
|
||||
*
|
||||
* @param partnerId 合作伙伴ID
|
||||
* @param quantity 采购数量
|
||||
* @return 实际划拨数量
|
||||
*/
|
||||
int purchase(String partnerId, int quantity);
|
||||
|
||||
/**
|
||||
* 消耗天卡(代销模式):从合作伙伴池子分配1张天卡,绑定到设备
|
||||
* <p>APK后续用同一设备标识调 /api/jst/init(免activationCode)+ checkBenefits 拿天卡播放</p>
|
||||
*
|
||||
* @param partnerId 合作伙伴ID(由PartnerSignFilter注入)
|
||||
* @param mac 设备MAC(与wifiMac/deviceUid/deviceSn至少传一个)
|
||||
* @param wifiMac WiFi MAC
|
||||
* @param deviceUid 设备唯一标识
|
||||
* @param deviceSn 设备序列号
|
||||
* @param channelOrgId 渠道组织ID(第三方直接传递渠道身份)
|
||||
* @return 天卡信息(deviceId/thirdCardNo/expireAt/status)
|
||||
*/
|
||||
Map<String, Object> consume(String partnerId, String mac, String wifiMac, String deviceUid, String deviceSn, Long channelOrgId);
|
||||
|
||||
/**
|
||||
* 回退天卡:把partner池子里未消耗(AVAILABLE)的天卡回退到平台天卡池
|
||||
*
|
||||
* @param partnerId 合作伙伴ID
|
||||
* @param quantity 回退数量
|
||||
* @return 实际回退数量
|
||||
*/
|
||||
int revert(String partnerId, int quantity);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package io.as.modules.openapi.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.Query;
|
||||
import io.as.modules.openapi.dao.ContentPackageDao;
|
||||
import io.as.modules.openapi.entity.ContentPackageEntity;
|
||||
import io.as.modules.openapi.service.ContentPackageService;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 开放API内容包授权
|
||||
*/
|
||||
@Service("contentPackageService")
|
||||
public class ContentPackageServiceImpl extends ServiceImpl<ContentPackageDao, ContentPackageEntity> implements ContentPackageService {
|
||||
|
||||
@Override
|
||||
public PageUtils queryPage(Map<String, Object> params) {
|
||||
String name = (String) params.get("name");
|
||||
String status = (String) params.get("status");
|
||||
|
||||
IPage<ContentPackageEntity> page = this.page(
|
||||
new Query<ContentPackageEntity>().getPage(params),
|
||||
new QueryWrapper<ContentPackageEntity>()
|
||||
.like(StringUtils.isNotBlank(name), "name", name)
|
||||
.eq(StringUtils.isNotBlank(status), "status", status)
|
||||
.eq("delete_flag", "UNDEL")
|
||||
.orderByDesc("created_at")
|
||||
);
|
||||
|
||||
return new PageUtils(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentPackageEntity getByPartnerId(String partnerId) {
|
||||
return this.getOne(
|
||||
new QueryWrapper<ContentPackageEntity>()
|
||||
.eq("partner_id", partnerId)
|
||||
.eq("status", "ACTIVE")
|
||||
.eq("delete_flag", "UNDEL")
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveOrUpdatePackage(ContentPackageEntity entity) {
|
||||
ContentPackageEntity exist = this.getOne(
|
||||
new QueryWrapper<ContentPackageEntity>()
|
||||
.eq("partner_id", entity.getPartnerId())
|
||||
.eq("delete_flag", "UNDEL")
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
if (exist != null) {
|
||||
entity.setId(exist.getId());
|
||||
this.updateById(entity);
|
||||
} else {
|
||||
this.save(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package io.as.modules.openapi.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.Query;
|
||||
import io.as.modules.openapi.dao.OpenApiCallLogDao;
|
||||
import io.as.modules.openapi.entity.OpenApiCallLogEntity;
|
||||
import io.as.modules.openapi.service.OpenApiCallLogService;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 开放API调用日志
|
||||
*/
|
||||
@Service("openApiCallLogService")
|
||||
public class OpenApiCallLogServiceImpl extends ServiceImpl<OpenApiCallLogDao, OpenApiCallLogEntity> implements OpenApiCallLogService {
|
||||
|
||||
@Override
|
||||
public PageUtils queryPage(Map<String, Object> params) {
|
||||
String partnerId = (String) params.get("partnerId");
|
||||
String api = (String) params.get("api");
|
||||
|
||||
IPage<OpenApiCallLogEntity> page = this.page(
|
||||
new Query<OpenApiCallLogEntity>().getPage(params),
|
||||
new QueryWrapper<OpenApiCallLogEntity>()
|
||||
.eq(StringUtils.isNotBlank(partnerId), "partner_id", partnerId)
|
||||
.like(StringUtils.isNotBlank(api), "api", api)
|
||||
.orderByDesc("created_at")
|
||||
);
|
||||
|
||||
return new PageUtils(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveLog(String partnerId, String appKey, String api, String method, String params, Integer resultCode, String ip, Integer costMs) {
|
||||
OpenApiCallLogEntity entity = new OpenApiCallLogEntity();
|
||||
entity.setPartnerId(partnerId);
|
||||
entity.setAppKey(appKey);
|
||||
entity.setApi(api);
|
||||
entity.setHttpMethod(method);
|
||||
entity.setParams(params);
|
||||
entity.setResultCode(resultCode);
|
||||
entity.setIp(ip);
|
||||
entity.setCostMs(costMs);
|
||||
entity.setCreatedAt(new Date());
|
||||
this.save(entity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package io.as.modules.openapi.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.Query;
|
||||
import io.as.modules.openapi.dao.OpenApiPartnerDao;
|
||||
import io.as.modules.openapi.entity.OpenApiPartnerEntity;
|
||||
import io.as.modules.openapi.service.OpenApiPartnerService;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 开放API合作伙伴
|
||||
*/
|
||||
@Service("openApiPartnerService")
|
||||
public class OpenApiPartnerServiceImpl extends ServiceImpl<OpenApiPartnerDao, OpenApiPartnerEntity> implements OpenApiPartnerService {
|
||||
|
||||
@Override
|
||||
public PageUtils queryPage(Map<String, Object> params) {
|
||||
String name = (String) params.get("name");
|
||||
String partnerId = (String) params.get("partnerId");
|
||||
String status = (String) params.get("status");
|
||||
|
||||
IPage<OpenApiPartnerEntity> page = this.page(
|
||||
new Query<OpenApiPartnerEntity>().getPage(params),
|
||||
new QueryWrapper<OpenApiPartnerEntity>()
|
||||
.like(StringUtils.isNotBlank(name), "name", name)
|
||||
.like(StringUtils.isNotBlank(partnerId), "partner_id", partnerId)
|
||||
.eq(StringUtils.isNotBlank(status), "status", status)
|
||||
.eq("delete_flag", "UNDEL")
|
||||
.orderByDesc("created_at")
|
||||
);
|
||||
|
||||
return new PageUtils(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenApiPartnerEntity getByAppKey(String appKey) {
|
||||
return this.getOne(
|
||||
new QueryWrapper<OpenApiPartnerEntity>()
|
||||
.eq("app_key", appKey)
|
||||
.eq("delete_flag", "UNDEL")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
package io.as.modules.openapi.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.Query;
|
||||
import io.as.common.utils.RedisUtils;
|
||||
import io.as.modules.daycard.constants.DayCardConstants;
|
||||
import io.as.modules.daycard.dao.DayCardPoolDao;
|
||||
import io.as.modules.daycard.entity.DayCardPoolEntity;
|
||||
import io.as.modules.device.dao.DeviceDao;
|
||||
import io.as.modules.device.entity.DeviceEntity;
|
||||
import io.as.modules.device.service.DeviceService;
|
||||
import io.as.modules.jst.service.JstActivateCardResult;
|
||||
import io.as.modules.jst.service.JushitingApiService;
|
||||
import io.as.modules.openapi.dao.PartnerCardPoolDao;
|
||||
import io.as.modules.openapi.entity.PartnerCardPoolEntity;
|
||||
import io.as.modules.openapi.service.PartnerCardPoolService;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.task.TaskExecutor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 合作伙伴天卡池
|
||||
*/
|
||||
@Service("partnerCardPoolService")
|
||||
public class PartnerCardPoolServiceImpl extends ServiceImpl<PartnerCardPoolDao, PartnerCardPoolEntity> implements PartnerCardPoolService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PartnerCardPoolServiceImpl.class);
|
||||
|
||||
@Autowired
|
||||
private DayCardPoolDao dayCardPoolDao;
|
||||
@Autowired
|
||||
private DeviceDao deviceDao;
|
||||
@Autowired
|
||||
private DeviceService deviceService;
|
||||
@Autowired
|
||||
private JushitingApiService jushitingApiService;
|
||||
@Autowired
|
||||
@Qualifier("thirdPartyActivateExecutor")
|
||||
private TaskExecutor activateExecutor;
|
||||
@Autowired
|
||||
private RedisUtils redisUtils;
|
||||
|
||||
@Override
|
||||
public PageUtils queryPage(Map<String, Object> params) {
|
||||
String partnerId = (String) params.get("partnerId");
|
||||
String status = (String) params.get("status");
|
||||
String purchaseBatch = (String) params.get("purchaseBatch");
|
||||
|
||||
IPage<PartnerCardPoolEntity> page = this.page(
|
||||
new Query<PartnerCardPoolEntity>().getPage(params),
|
||||
new QueryWrapper<PartnerCardPoolEntity>()
|
||||
.eq(StringUtils.isNotBlank(partnerId), "partner_id", partnerId)
|
||||
.eq(StringUtils.isNotBlank(status), "status", status)
|
||||
.eq(StringUtils.isNotBlank(purchaseBatch), "purchase_batch", purchaseBatch)
|
||||
.eq("delete_flag", "UNDEL")
|
||||
.orderByDesc("created_at")
|
||||
);
|
||||
return new PageUtils(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAvailableCount(String partnerId) {
|
||||
return Math.toIntExact(this.count(
|
||||
new QueryWrapper<PartnerCardPoolEntity>()
|
||||
.eq("partner_id", partnerId)
|
||||
.eq("status", "AVAILABLE")
|
||||
.eq("delete_flag", "UNDEL")
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PartnerCardPoolEntity> getAvailableCards(String partnerId, int limit) {
|
||||
return this.list(
|
||||
new QueryWrapper<PartnerCardPoolEntity>()
|
||||
.eq("partner_id", partnerId)
|
||||
.eq("status", "AVAILABLE")
|
||||
.eq("delete_flag", "UNDEL")
|
||||
.orderByAsc("id")
|
||||
.last("LIMIT " + limit)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PartnerCardPoolEntity getByPoolId(Long poolId) {
|
||||
return this.getOne(
|
||||
new QueryWrapper<PartnerCardPoolEntity>()
|
||||
.eq("pool_id", poolId)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveBatchCards(String partnerId, String purchaseBatch, List<PartnerCardPoolEntity> cards) {
|
||||
this.saveBatch(cards);
|
||||
}
|
||||
|
||||
// ==================== 采购 ====================
|
||||
|
||||
@Override
|
||||
public int purchase(String partnerId, int quantity) {
|
||||
if (quantity <= 0) {
|
||||
throw new ASException("采购数量必须大于0", 400);
|
||||
}
|
||||
// 1. 从平台天卡池取 quantity 张 UNUSED 的卡(SKIP LOCKED 防阻塞)
|
||||
List<DayCardPoolEntity> cards = dayCardPoolDao.selectList(
|
||||
new QueryWrapper<DayCardPoolEntity>()
|
||||
.eq("status", DayCardConstants.POOL_STATUS_UNUSED)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.last("LIMIT " + quantity + " FOR UPDATE SKIP LOCKED")
|
||||
);
|
||||
if (cards.isEmpty()) {
|
||||
throw new ASException("平台天卡池库存不足", 400);
|
||||
}
|
||||
|
||||
String purchaseBatch = "PUR" + System.currentTimeMillis();
|
||||
Date now = new Date();
|
||||
for (DayCardPoolEntity card : cards) {
|
||||
// 2. 平台卡状态 UNUSED -> RESERVED(划拨给partner)
|
||||
int updated = dayCardPoolDao.update(null, new UpdateWrapper<DayCardPoolEntity>()
|
||||
.eq("id", card.getId())
|
||||
.eq("status", DayCardConstants.POOL_STATUS_UNUSED)
|
||||
.set("status", DayCardConstants.POOL_STATUS_RESERVED)
|
||||
.set("updated_at", now)
|
||||
);
|
||||
if (updated == 0) {
|
||||
continue; // 被其他线程抢走,跳过
|
||||
}
|
||||
// 3. 记录到 partner 池子
|
||||
PartnerCardPoolEntity pcp = new PartnerCardPoolEntity();
|
||||
pcp.setPartnerId(partnerId);
|
||||
pcp.setPoolId(card.getId());
|
||||
pcp.setCardCode(card.getCardCode());
|
||||
pcp.setThirdCardNo(card.getThirdCardNo());
|
||||
pcp.setStatus("AVAILABLE");
|
||||
pcp.setPurchaseBatch(purchaseBatch);
|
||||
pcp.setDeleteFlag("UNDEL");
|
||||
pcp.setCreatedAt(now);
|
||||
pcp.setUpdatedAt(now);
|
||||
this.save(pcp);
|
||||
}
|
||||
logger.info("合作伙伴采购天卡: partnerId={}, quantity={}, batch={}", partnerId, cards.size(), purchaseBatch);
|
||||
return cards.size();
|
||||
}
|
||||
|
||||
// ==================== 消耗(代销模式) ====================
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> consume(String partnerId, String mac, String wifiMac, String deviceUid, String deviceSn, Long channelOrgId) {
|
||||
if (StringUtils.isBlank(mac) && StringUtils.isBlank(wifiMac)
|
||||
&& StringUtils.isBlank(deviceUid) && StringUtils.isBlank(deviceSn)) {
|
||||
throw new ASException("mac/wifiMac/deviceUid/deviceSn 至少传一个", 400);
|
||||
}
|
||||
|
||||
// 1. Redis防重锁:按设备标识加锁,防止并发重复消耗
|
||||
String lockKey = DayCardConstants.REDIS_PARTNER_CONSUME_LOCK_PREFIX
|
||||
+ (StringUtils.isNotBlank(mac) ? mac
|
||||
: StringUtils.isNotBlank(wifiMac) ? wifiMac
|
||||
: StringUtils.isNotBlank(deviceUid) ? deviceUid : deviceSn);
|
||||
boolean locked = redisUtils.setIfAbsent(lockKey, "1", 30);
|
||||
if (!locked) {
|
||||
throw new ASException("操作太频繁,请稍后再试", 429);
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. 查找/创建设备
|
||||
DeviceEntity device = findOrCreateDevice(mac, wifiMac, deviceUid, deviceSn);
|
||||
boolean isNew = device.getId() == null;
|
||||
Date now = new Date();
|
||||
if (isNew) {
|
||||
if (StringUtils.isNotBlank(mac)) device.setMac(mac);
|
||||
if (StringUtils.isNotBlank(wifiMac)) device.setWifiMac(wifiMac);
|
||||
if (StringUtils.isNotBlank(deviceUid)) device.setDeviceUid(deviceUid);
|
||||
if (StringUtils.isNotBlank(deviceSn)) device.setDeviceSn(deviceSn);
|
||||
device.setLastHeartbeat(now);
|
||||
device.setCreatedAt(now);
|
||||
device.setDeleteFlag("UNDEL");
|
||||
device.setStatus("ACTIVE");
|
||||
device.setSource("PARTNER");
|
||||
device.setPartnerId(partnerId);
|
||||
deviceService.saveDevice(device);
|
||||
}
|
||||
|
||||
// 3. 幂等检查:设备已有有效天卡则直接返回(防重复消耗)
|
||||
DayCardPoolEntity existing = getDeviceActiveCard(device.getId());
|
||||
if (existing != null) {
|
||||
Map<String, Object> idempotent = new HashMap<>();
|
||||
idempotent.put("deviceId", device.getId());
|
||||
idempotent.put("thirdCardNo", existing.getThirdCardNo());
|
||||
idempotent.put("expireAt", existing.getExpireAt());
|
||||
idempotent.put("status", "ACTIVE");
|
||||
logger.info("代销天卡消耗幂等返回: deviceId={}, poolId={}", device.getId(), existing.getId());
|
||||
return idempotent;
|
||||
}
|
||||
|
||||
// 4. 从 partner 池子取一张可用天卡(FOR UPDATE SKIP LOCKED 防并发超卖)
|
||||
PartnerCardPoolEntity pcp = this.getOne(
|
||||
new QueryWrapper<PartnerCardPoolEntity>()
|
||||
.eq("partner_id", partnerId)
|
||||
.eq("status", "AVAILABLE")
|
||||
.eq("delete_flag", "UNDEL")
|
||||
.orderByAsc("id")
|
||||
.last("LIMIT 1 FOR UPDATE SKIP LOCKED")
|
||||
);
|
||||
if (pcp == null) {
|
||||
throw new ASException("天卡池库存不足", 400);
|
||||
}
|
||||
|
||||
// 5. 绑定天卡到设备:day_card_pool RESERVED -> USED + deviceId + expireAt(默认24h)
|
||||
Date defaultExpire = new Date(now.getTime() + 24 * 60 * 60 * 1000L);
|
||||
int updated = dayCardPoolDao.update(null, new UpdateWrapper<DayCardPoolEntity>()
|
||||
.eq("id", pcp.getPoolId())
|
||||
.eq("status", DayCardConstants.POOL_STATUS_RESERVED)
|
||||
.set("status", DayCardConstants.POOL_STATUS_USED)
|
||||
.set("device_id", device.getId())
|
||||
.set("activate_status", DayCardConstants.ACTIVATE_STATUS_PENDING)
|
||||
.set("used_at", now)
|
||||
.set("expire_at", defaultExpire)
|
||||
.set("updated_at", now)
|
||||
);
|
||||
if (updated == 0) {
|
||||
throw new ASException("天卡分配失败,请重试", 500);
|
||||
}
|
||||
|
||||
// 6. 更新 partner 池子:CONSUMED + deviceId + channelOrgId
|
||||
pcp.setStatus("CONSUMED");
|
||||
pcp.setDeviceId(device.getId());
|
||||
pcp.setChannelOrgId(channelOrgId);
|
||||
pcp.setConsumedAt(now);
|
||||
pcp.setUpdatedAt(now);
|
||||
this.updateById(pcp);
|
||||
|
||||
// 7. 异步激活天卡(调橘视厅)
|
||||
final Long poolId = pcp.getPoolId();
|
||||
final Long deviceId = device.getId();
|
||||
activateExecutor.execute(() -> activateCardAsync(poolId, deviceId));
|
||||
|
||||
// 8. 返回天卡信息(异步激活中,客户端通过checkBenefits轮询确认)
|
||||
// thirdCardNo不暴露给第三方,App通过init+checkBenefits自行获取播放凭证
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("deviceId", device.getId());
|
||||
result.put("expireAt", defaultExpire);
|
||||
result.put("status", "ACTIVATING");
|
||||
logger.info("代销天卡消耗: partnerId={}, deviceId={}, poolId={}, thirdCardNo={}",
|
||||
partnerId, device.getId(), poolId, pcp.getThirdCardNo());
|
||||
return result;
|
||||
} finally {
|
||||
redisUtils.delete(lockKey);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 回退 ====================
|
||||
|
||||
@Override
|
||||
public int revert(String partnerId, int quantity) {
|
||||
if (quantity <= 0) {
|
||||
throw new ASException("回退数量必须大于0", 400);
|
||||
}
|
||||
List<PartnerCardPoolEntity> cards = getAvailableCards(partnerId, quantity);
|
||||
if (cards.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
Date now = new Date();
|
||||
for (PartnerCardPoolEntity card : cards) {
|
||||
// 平台天卡池 RESERVED -> UNUSED(恢复可分配)
|
||||
dayCardPoolDao.update(null, new UpdateWrapper<DayCardPoolEntity>()
|
||||
.eq("id", card.getPoolId())
|
||||
.eq("status", DayCardConstants.POOL_STATUS_RESERVED)
|
||||
.set("status", DayCardConstants.POOL_STATUS_UNUSED)
|
||||
.set("updated_at", now)
|
||||
);
|
||||
// partner池子标记DISABLED
|
||||
card.setStatus("DISABLED");
|
||||
card.setUpdatedAt(now);
|
||||
this.updateById(card);
|
||||
}
|
||||
logger.info("合作伙伴回退天卡: partnerId={}, quantity={}", partnerId, cards.size());
|
||||
return cards.size();
|
||||
}
|
||||
|
||||
/** 按设备标识查找已有设备,找不到返回新实体(标识优先级:mac > wifiMac > deviceUid > deviceSn) */
|
||||
private DeviceEntity findOrCreateDevice(String mac, String wifiMac, String deviceUid, String deviceSn) {
|
||||
DeviceEntity existing = null;
|
||||
if (StringUtils.isNotBlank(mac)) {
|
||||
existing = deviceDao.selectOne(new QueryWrapper<DeviceEntity>().eq("mac", mac).eq("delete_flag", "UNDEL").last("LIMIT 1"));
|
||||
}
|
||||
if (existing == null && StringUtils.isNotBlank(wifiMac)) {
|
||||
existing = deviceDao.selectOne(new QueryWrapper<DeviceEntity>().eq("wifi_mac", wifiMac).eq("delete_flag", "UNDEL").last("LIMIT 1"));
|
||||
}
|
||||
if (existing == null && StringUtils.isNotBlank(deviceUid)) {
|
||||
existing = deviceDao.selectOne(new QueryWrapper<DeviceEntity>().eq("device_uid", deviceUid).eq("delete_flag", "UNDEL").last("LIMIT 1"));
|
||||
}
|
||||
if (existing == null && StringUtils.isNotBlank(deviceSn)) {
|
||||
existing = deviceDao.selectOne(new QueryWrapper<DeviceEntity>().eq("device_sn", deviceSn).eq("delete_flag", "UNDEL").last("LIMIT 1"));
|
||||
}
|
||||
return existing != null ? existing : new DeviceEntity();
|
||||
}
|
||||
|
||||
/** 查询设备当前有效的天卡(status=USED 且未过期) */
|
||||
private DayCardPoolEntity getDeviceActiveCard(Long deviceId) {
|
||||
return dayCardPoolDao.selectOne(new QueryWrapper<DayCardPoolEntity>()
|
||||
.eq("device_id", deviceId)
|
||||
.eq("status", DayCardConstants.POOL_STATUS_USED)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.isNotNull("expire_at")
|
||||
.gt("expire_at", new Date())
|
||||
.orderByDesc("used_at")
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
/** 异步激活天卡:调橘视厅 getToken + activateCard,更新真实过期时间 */
|
||||
private void activateCardAsync(Long poolId, Long deviceId) {
|
||||
// 激活锁:防止同一张卡重复激活(参考生产项目 api:thirdparty:activate:lock:{cardId})
|
||||
String lockKey = DayCardConstants.REDIS_THIRD_PARTY_ACTIVATE_LOCK_PREFIX + poolId;
|
||||
boolean locked = redisUtils.setIfAbsent(lockKey, "1", 3600);
|
||||
if (!locked) {
|
||||
logger.info("天卡激活任务已存在,跳过: poolId={}", poolId);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
DayCardPoolEntity pool = dayCardPoolDao.selectById(poolId);
|
||||
if (pool == null) {
|
||||
logger.warn("异步激活失败:天卡不存在, poolId={}", poolId);
|
||||
return;
|
||||
}
|
||||
DeviceEntity device = deviceDao.selectById(deviceId);
|
||||
if (device == null) {
|
||||
updateActivateStatus(poolId, DayCardConstants.ACTIVATE_STATUS_FAILED, "设备不存在: " + deviceId, null);
|
||||
return;
|
||||
}
|
||||
String thirdCardRef = pool.getThirdCardNo();
|
||||
if (StringUtils.isBlank(thirdCardRef)) {
|
||||
updateActivateStatus(poolId, DayCardConstants.ACTIVATE_STATUS_FAILED, "第三方卡号为空", null);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 免密登录获取Token(优先从缓存读取,参考生产项目)
|
||||
String token;
|
||||
String memberCode;
|
||||
String tokenCacheKey = DayCardConstants.REDIS_THIRD_PARTY_TOKEN_PREFIX + thirdCardRef;
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> cached = redisUtils.get(tokenCacheKey, Map.class);
|
||||
if (cached != null && StringUtils.isNotBlank(cached.get("token"))
|
||||
&& StringUtils.isNotBlank(cached.get("memberCode"))) {
|
||||
token = cached.get("token");
|
||||
memberCode = cached.get("memberCode");
|
||||
} else {
|
||||
Map<String, String> tokenResult = jushitingApiService.getToken(device, thirdCardRef);
|
||||
if (tokenResult == null || StringUtils.isBlank(tokenResult.get("token"))) {
|
||||
updateActivateStatus(poolId, DayCardConstants.ACTIVATE_STATUS_FAILED, "获取Token失败", null);
|
||||
return;
|
||||
}
|
||||
token = tokenResult.get("token");
|
||||
memberCode = tokenResult.get("memberCode");
|
||||
// 缓存token和memberCode,5分钟有效
|
||||
Map<String, String> cacheData = new HashMap<>();
|
||||
cacheData.put("token", token);
|
||||
cacheData.put("memberCode", memberCode);
|
||||
redisUtils.set(tokenCacheKey, cacheData, 5 * 60);
|
||||
}
|
||||
|
||||
// 2. 卡密激活
|
||||
JstActivateCardResult activateResult = jushitingApiService.activateCard(
|
||||
deviceId, pool.getCardCode(), memberCode, token);
|
||||
if (!activateResult.isSuccess()) {
|
||||
String errMsg = activateResult.getErrorMessage() != null ? activateResult.getErrorMessage() : "卡密激活返回失败";
|
||||
updateActivateStatus(poolId, DayCardConstants.ACTIVATE_STATUS_FAILED, errMsg, null);
|
||||
return;
|
||||
}
|
||||
// 3. 确定过期时间
|
||||
Date expireAt = null;
|
||||
if (StringUtils.isNotBlank(activateResult.getExpireTime())) {
|
||||
expireAt = parseExpireTime(activateResult.getExpireTime());
|
||||
}
|
||||
if (expireAt == null) {
|
||||
String vipExpire = jushitingApiService.queryVipExpireTime(deviceId, thirdCardRef);
|
||||
if (StringUtils.isNotBlank(vipExpire)) {
|
||||
expireAt = parseExpireTime(vipExpire);
|
||||
}
|
||||
}
|
||||
if (expireAt == null) {
|
||||
expireAt = new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000L);
|
||||
}
|
||||
// 4. 更新过期时间 + 激活成功
|
||||
updateActivateStatus(poolId, DayCardConstants.ACTIVATE_STATUS_SUCCESS, null, expireAt);
|
||||
logger.info("代销天卡激活完成: poolId={}, deviceId={}, expireAt={}", poolId, deviceId, expireAt);
|
||||
} catch (Exception e) {
|
||||
logger.error("代销天卡激活异常: poolId={}", poolId, e);
|
||||
updateActivateStatus(poolId, DayCardConstants.ACTIVATE_STATUS_FAILED, e.getMessage(), null);
|
||||
} finally {
|
||||
try {
|
||||
redisUtils.delete(lockKey);
|
||||
} catch (Exception e) {
|
||||
logger.warn("释放激活锁失败: key={}", lockKey, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateActivateStatus(Long poolId, String status, String error, Date expireAt) {
|
||||
UpdateWrapper<DayCardPoolEntity> uw = new UpdateWrapper<>();
|
||||
uw.eq("id", poolId)
|
||||
.set("activate_status", status)
|
||||
.set("activate_error", error)
|
||||
.set("updated_at", new Date());
|
||||
if (expireAt != null) {
|
||||
uw.set("expire_at", expireAt).set("activated_at", new Date());
|
||||
}
|
||||
dayCardPoolDao.update(null, uw);
|
||||
}
|
||||
|
||||
private static Date parseExpireTime(String time) {
|
||||
if (time == null || time.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
java.util.TimeZone tz = java.util.TimeZone.getTimeZone("Asia/Shanghai");
|
||||
try {
|
||||
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
sdf.setTimeZone(tz);
|
||||
return sdf.parse(time);
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
|
||||
sdf.setTimeZone(tz);
|
||||
return sdf.parse(time);
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user