@@ -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 ;
}
}
}
}