package io.as.modules.openapi.controller; import io.as.common.annotation.AuditAction; import io.as.common.exception.ASException; import io.as.modules.daycard.constants.DayCardConstants; import io.as.modules.daycard.entity.DayCardActivationCodeEntity; import io.as.modules.daycard.entity.DayCardOrderEntity; import io.as.modules.daycard.entity.DayCardPoolEntity; import io.as.modules.daycard.entity.DayCardPriceConfigEntity; import io.as.modules.daycard.service.DayCardActivationCodeService; import io.as.modules.daycard.service.DayCardOrderService; import io.as.modules.device.entity.DeviceEntity; import io.as.modules.device.service.DeviceService; import io.as.modules.openapi.filter.ApiDeviceSignFilter; import io.as.modules.openapi.form.DeviceInitRequest; import io.as.modules.openapi.sign.ApiDeviceSignService; import io.as.modules.openapi.sign.DeviceSignResult; import io.as.modules.sys.entity.SysOrgEntity; import io.as.modules.sys.service.SysOrgService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * 橘视听 API 设备接口(对齐生产环境 as-partner-backend 的 ApiJstController) *

完整流程:

*
    *
  1. init:设备初始化(注册/更新),返回 sign
  2. *
  3. checkBenefits:查询天卡权益,返回 thirdCardNo 与 status(ACTIVE/UNBOUND)
  4. *
  5. 无权益 → daycard/create 创建天卡订单 → 支付 → 轮询 checkBenefits 拿 thirdCardNo
  6. *
*

鉴权:除 POST /api/jst/init 外,均须在请求头携带 sign。缺少或无效 sign 由过滤器直接返回 HTTP 401。

*/ @Api(tags = "橘视听-API-设备接口", description = "除 POST /api/jst/init 外均需请求头 sign;缺/无效 sign→401。业务结果以 JSON 的 code 为准。") @RestController @RequestMapping("/api/jst") public class ApiJstController { private static final Logger logger = LoggerFactory.getLogger(ApiJstController.class); @Autowired private ApiDeviceSignService apiDeviceSignService; @Autowired private DeviceService deviceService; @Autowired private SysOrgService sysOrgService; @Autowired private DayCardOrderService dayCardOrderService; @Autowired private DayCardActivationCodeService dayCardActivationCodeService; /** * 设备初始化:校验设备是否在系统,不在则添加、在则更新,返回 sign */ @ApiOperation(value = "设备初始化", notes = "本接口无需 sign。body 必传 activationCode(管理后台按门店生成的激活码);设备可传 mac、wifiMac、deviceSn、deviceUid 等。成功返回 data.device、sign。") @AuditAction(value = "DEVICE_INIT", description = "设备初始化") @PostMapping("/init") public Map init( @ApiParam(value = "设备初始化请求体", required = true) @RequestBody DeviceInitRequest request) { try { return doInit(request); } catch (ASException e) { return rrExceptionResponse(e); } } /** * 设备权益状态:查询天卡权益,返回 thirdCardNo + status *

status: ACTIVE(橘视厅激活成功且未过期,返回 thirdCardNo)/ ACTIVATING(激活中)/ ACTIVATION_FAILED(激活失败)/ UNBOUND(无天卡)

*/ @ApiOperation(value = "设备权益状态", notes = "请求头必传 sign。查询设备天卡权益,返回 data.status(ACTIVE/ACTIVATING/ACTIVATION_FAILED/UNBOUND)。仅 ACTIVE 时返回 data.thirdCardNo(播放凭证)和 data.expireAt。") @ApiImplicitParams({ @ApiImplicitParam(name = "sign", value = "设备签名(用于标识设备)", required = true, paramType = "header", dataType = "string") }) @PostMapping("/checkBenefits") public Map checkBenefits(HttpServletRequest request) { Long deviceId = deviceIdFrom(request); DayCardPoolEntity pool = dayCardOrderService.getDeviceBenefits(deviceId); Map data = new HashMap<>(); if (pool == null) { data.put("status", "UNBOUND"); } else if (DayCardConstants.ACTIVATE_STATUS_SUCCESS.equals(pool.getActivateStatus())) { data.put("status", "ACTIVE"); data.put("thirdCardNo", pool.getThirdCardNo()); data.put("cardNo", pool.getCardCode()); data.put("expireAt", pool.getExpireAt()); } else if (DayCardConstants.ACTIVATE_STATUS_FAILED.equals(pool.getActivateStatus())) { data.put("status", "ACTIVATION_FAILED"); data.put("activateError", pool.getActivateError()); } else { // PENDING 或 null,激活中 data.put("status", "ACTIVATING"); } return buildSuccessResponse(data); } /** * 设备天卡价格:供前端二维码展示 *

按 3 层维度匹配当前设备生效价:门店配置价 > 渠道配置价 > 平台默认价

*/ @ApiOperation(value = "设备天卡价格", notes = "请求头必传 sign。返回 data.price(元)、data.priceFen(分)、data.name。未配置定价时 data.status=UNCONFIGURED。") @ApiImplicitParams({ @ApiImplicitParam(name = "sign", value = "设备签名", required = true, paramType = "header", dataType = "string") }) @PostMapping("/price") public Map price(HttpServletRequest request) { Long deviceId = deviceIdFrom(request); DayCardPriceConfigEntity config = dayCardOrderService.getDevicePrice(deviceId); Map data = new HashMap<>(); if (config == null) { data.put("status", "UNCONFIGURED"); } else { data.put("status", "OK"); data.put("price", config.getPrice()); data.put("priceFen", config.getPrice().multiply(new java.math.BigDecimal("100")).intValue()); data.put("name", config.getName()); } return buildSuccessResponse(data); } /** * 创建天卡订单(无权益时调用) */ @ApiOperation(value = "创建天卡订单", notes = "请求头必传 sign。从 sign 解析 deviceId,无需传设备标识。返回 data.orderNo、data.status、data.amount(分)、data.codeUrl、data.expireAt。") @AuditAction(value = "DAYCARD_ORDER_CREATE", description = "创建天卡订单(设备端)") @ApiImplicitParams({ @ApiImplicitParam(name = "sign", value = "设备签名", required = true, paramType = "header", dataType = "string") }) @PostMapping("/daycard/create") public Map createDayCard(HttpServletRequest request) { try { Long deviceId = deviceIdFrom(request); DayCardOrderEntity order = dayCardOrderService.createOrder(deviceId); Map data = new HashMap<>(); data.put("orderNo", order.getOrderNo()); data.put("status", order.getStatus()); data.put("amount", order.getAmount().multiply(new java.math.BigDecimal("100")).intValue()); data.put("cardNo", order.getCardNo()); data.put("codeUrl", order.getCodeUrl()); data.put("expireAt", order.getExpireTime()); data.put("deviceId", order.getDeviceId()); return buildSuccessResponse(data); } catch (ASException e) { return rrExceptionResponse(e); } } /** * 查询支付状态 */ @ApiOperation(value = "查询支付状态", notes = "请求头必传 sign。路径参数为订单号,返回 data.status(CREATED/PAYING/PAID/CANCELLED)、data.codeUrl 等。") @ApiImplicitParams({ @ApiImplicitParam(name = "sign", value = "设备签名", required = true, paramType = "header", dataType = "string") }) @GetMapping("/daycard/status/{orderNo}") public Map daycardStatus(@PathVariable("orderNo") String orderNo) { DayCardOrderEntity order = dayCardOrderService.getByOrderNo(orderNo); if (order == null) { Map err = new HashMap<>(); err.put("code", 404); err.put("msg", "订单不存在"); err.put("data", new HashMap<>()); return err; } Map data = new HashMap<>(); data.put("orderNo", order.getOrderNo()); data.put("status", order.getStatus()); data.put("paidAt", order.getPayTime()); data.put("codeUrl", order.getCodeUrl()); data.put("amount", order.getAmount() != null ? order.getAmount().multiply(new java.math.BigDecimal("100")).intValue() : 0); data.put("payChannel", order.getPayChannel()); return buildSuccessResponse(data); } /** * 取消天卡订单(用户放弃支付时调用) */ @ApiOperation(value = "取消天卡订单", notes = "请求头必传 sign。用户取消支付时调用,取消订单并释放卡,允许设备重新下单。") @ApiImplicitParams({ @ApiImplicitParam(name = "sign", value = "设备签名", required = true, paramType = "header", dataType = "string") }) @PostMapping("/daycard/cancel/{orderNo}") public Map cancelDayCard(@PathVariable("orderNo") String orderNo, HttpServletRequest request) { try { Long deviceId = deviceIdFrom(request); dayCardOrderService.cancelOrder(orderNo, deviceId); Map data = new HashMap<>(); data.put("orderNo", orderNo); data.put("status", "CANCELLED"); return buildSuccessResponse(data); } catch (ASException e) { return rrExceptionResponse(e); } } /** * 设备心跳:更新设备心跳时间,续期 sign */ @ApiOperation(value = "设备心跳", notes = "请求头必传 sign。更新设备 last_heartbeat 并续期 sign 缓存 TTL。建议按业务间隔调用以防 sign 过期。") @ApiImplicitParams({ @ApiImplicitParam(name = "sign", value = "设备签名", required = true, paramType = "header", dataType = "string") }) @PostMapping("/heartBeat") public Map heartBeat(HttpServletRequest request) { Long deviceId = deviceIdFrom(request); DeviceEntity device = deviceService.getById(deviceId); if (device != null) { device.setLastHeartbeat(new Date()); deviceService.updateDevice(device); } apiDeviceSignService.refreshExpire(deviceId); return buildSuccessResponse(new HashMap<>()); } /** * 刷新 sign:换新 sign,旧 sign 立即失效 */ @ApiOperation(value = "刷新 sign", notes = "请求头必传当前有效 sign。成功后在 data 中返回新 sign;旧 sign 立即失效,客户端必须替换本地存储。") @ApiImplicitParams({ @ApiImplicitParam(name = "sign", value = "设备签名(旧的即将过期 sign)", required = true, paramType = "header", dataType = "string") }) @PostMapping("/refreshSign") public Map refreshSign(HttpServletRequest request) { Long deviceId = deviceIdFrom(request); String orgCode = channelOrgCodeFrom(request); String newSign = apiDeviceSignService.generate(deviceId, System.currentTimeMillis(), orgCode); Map data = new HashMap<>(); data.put("sign", newSign); return buildSuccessResponse(data); } // ==================== 内部方法 ==================== private Map doInit(DeviceInitRequest request) { if (request == null) { throw new ASException("请求体不能为空", 400); } String activationCode = trim(request.getActivationCode()); if (StringUtils.isBlank(activationCode)) { // 代销模式:免activationCode,按设备标识查找已注册设备(由partner消耗天卡接口创建) return doInitWithoutActivationCode(request); } // 通过激活码解析门店与渠道 DayCardActivationCodeEntity codeEntity = dayCardActivationCodeService.getByCode(activationCode); if (codeEntity == null) { throw new ASException("激活码无效或已禁用", 400); } Long storeOrgId = codeEntity.getStoreOrgId(); Long channelOrgId = codeEntity.getChannelOrgId(); if (storeOrgId == null) { throw new ASException("激活码未绑定门店", 400); } // 校验门店存在 SysOrgEntity storeOrg = sysOrgService.getById(storeOrgId); if (storeOrg == null) { throw new ASException("门店不存在", 400); } // 校验设备数上限 if (codeEntity.getMaxDeviceCount() != null && codeEntity.getMaxDeviceCount() > 0) { int activated = codeEntity.getActivatedCount() != null ? codeEntity.getActivatedCount() : 0; if (activated >= codeEntity.getMaxDeviceCount()) { throw new ASException("该激活码已达设备上限(" + codeEntity.getMaxDeviceCount() + "台)", 400); } } String androidId = trim(request.getAndroidId()); String mac = trim(request.getMac()); String wifiMac = trim(request.getWifiMac()); String deviceSn = trim(request.getDeviceSn()); String deviceUid = trim(request.getDeviceUid()); // 查找或创建设备(按优先级降权匹配:androidId > mac > deviceSn > wifiMac > deviceUid) DeviceEntity device = findOrCreateDevice(androidId, mac, wifiMac, deviceSn, deviceUid); boolean isNewDevice = device.getId() == null; Date now = new Date(); device.setStoreOrgId(storeOrgId); // 仅用非空标识更新,避免覆盖已有标识(按文档"更新缺失的标识字段") if (StringUtils.isNotBlank(androidId)) device.setAndroidId(androidId); if (StringUtils.isNotBlank(mac)) device.setMac(mac); if (StringUtils.isNotBlank(wifiMac)) device.setWifiMac(wifiMac); if (StringUtils.isNotBlank(deviceSn)) device.setDeviceSn(deviceSn); if (StringUtils.isNotBlank(deviceUid)) device.setDeviceUid(deviceUid); if (StringUtils.isNotBlank(request.getManufacturer())) device.setManufacturer(trim(request.getManufacturer())); if (StringUtils.isNotBlank(request.getModel())) device.setModel(trim(request.getModel())); if (StringUtils.isNotBlank(request.getBrand())) device.setBrand(trim(request.getBrand())); if (StringUtils.isNotBlank(request.getVerCode())) device.setVerCode(trim(request.getVerCode())); if (StringUtils.isNotBlank(request.getVerName())) device.setVerName(trim(request.getVerName())); if (StringUtils.isNotBlank(request.getAndroidVersion())) device.setAndroidVersion(trim(request.getAndroidVersion())); if (StringUtils.isNotBlank(request.getDeviceModel())) device.setDeviceModel(trim(request.getDeviceModel())); if (StringUtils.isNotBlank(request.getDeviceName())) device.setDeviceName(trim(request.getDeviceName())); device.setLastHeartbeat(now); if (isNewDevice) { device.setCreatedAt(now); device.setDeleteFlag("UNDEL"); device.setStatus("ACTIVE"); deviceService.saveDevice(device); // 新设备激活计数 +1 dayCardActivationCodeService.incrementActivatedCount(codeEntity.getId()); } else { device.setUpdatedAt(now); deviceService.updateDevice(device); } // 生成 sign(orgCode 用渠道组织ID的字符串形式,sign 校验时不依赖此值) String orgCode = channelOrgId != null ? String.valueOf(channelOrgId) : ""; String sign = apiDeviceSignService.generate(device.getId(), System.currentTimeMillis(), orgCode); Map data = new HashMap<>(); Map deviceInfo = new HashMap<>(); deviceInfo.put("deviceId", device.getId()); deviceInfo.put("roomNo", device.getRoomNo()); data.put("device", deviceInfo); data.put("sign", sign); return buildSuccessResponse(data); } /** * 代销模式免activationCode初始化:按设备标识查找已注册设备,返回sign *

设备由 /openapi/partner/card/consume 接口创建,APK用同一设备标识调本方法拿sign

*/ private Map doInitWithoutActivationCode(DeviceInitRequest request) { String mac = trim(request.getMac()); String wifiMac = trim(request.getWifiMac()); String deviceSn = trim(request.getDeviceSn()); String deviceUid = trim(request.getDeviceUid()); if (StringUtils.isBlank(mac) && StringUtils.isBlank(wifiMac) && StringUtils.isBlank(deviceSn) && StringUtils.isBlank(deviceUid)) { throw new ASException("代销模式免激活码,需至少传一个设备标识(mac/wifiMac/deviceSn/deviceUid)", 400); } // 按优先级查找已有设备:mac > wifiMac > deviceSn > deviceUid(与生产环境一致) DeviceEntity device = findByIdentifier(mac, DeviceEntity::getMac); if (device == null) device = findByIdentifier(wifiMac, DeviceEntity::getWifiMac); if (device == null) device = findByIdentifier(deviceSn, DeviceEntity::getDeviceSn); if (device == null) device = findByIdentifier(deviceUid, DeviceEntity::getDeviceUid); if (device == null) { throw new ASException("设备未注册,请先通过合作伙伴天卡消耗接口激活设备", 400); } // 更新心跳 device.setLastHeartbeat(new Date()); deviceService.updateDevice(device); // 生成sign(orgCode留空,代销模式不依赖渠道组织) String sign = apiDeviceSignService.generate(device.getId(), System.currentTimeMillis(), ""); Map data = new HashMap<>(); Map deviceInfo = new HashMap<>(); deviceInfo.put("deviceId", device.getId()); deviceInfo.put("roomNo", device.getRoomNo()); data.put("device", deviceInfo); Map result = buildSuccessResponse(data); result.put("sign", sign); return result; } /** * 查找或创建设备:按 mac 优先查找,无则新建 *

注:此处为简化实现,完整的多策略识别算法在 DeviceResolveService 中

*/ private DeviceEntity findOrCreateDevice(String androidId, String mac, String wifiMac, String deviceSn, String deviceUid) { if (StringUtils.isBlank(androidId) && StringUtils.isBlank(mac) && StringUtils.isBlank(wifiMac) && StringUtils.isBlank(deviceSn) && StringUtils.isBlank(deviceUid)) { throw new ASException("androidId、mac、wifiMac、deviceSn、deviceUid 至少传一个", 400); } // 按优先级降权匹配:androidId > mac > deviceSn > wifiMac > deviceUid DeviceEntity existing = findByIdentifier(androidId, DeviceEntity::getAndroidId); if (existing == null) existing = findByIdentifier(mac, DeviceEntity::getMac); if (existing == null) existing = findByIdentifier(deviceSn, DeviceEntity::getDeviceSn); if (existing == null) existing = findByIdentifier(wifiMac, DeviceEntity::getWifiMac); if (existing == null) existing = findByIdentifier(deviceUid, DeviceEntity::getDeviceUid); return existing != null ? existing : new DeviceEntity(); } private DeviceEntity findByIdentifier(String value, com.baomidou.mybatisplus.core.toolkit.support.SFunction column) { if (StringUtils.isBlank(value)) { return null; } return deviceService.lambdaQuery() .eq(column, value) .eq(DeviceEntity::getDeleteFlag, "UNDEL") .last("LIMIT 1").one(); } private static Long deviceIdFrom(HttpServletRequest request) { return (Long) request.getAttribute(ApiDeviceSignFilter.REQUEST_ATTR_DEVICE_ID); } private static String channelOrgCodeFrom(HttpServletRequest request) { DeviceSignResult signResult = (DeviceSignResult) request.getAttribute(ApiDeviceSignFilter.REQUEST_ATTR_DEVICE_SIGN_RESULT); return signResult != null ? signResult.getOrgCode() : null; } private static String trim(String s) { return s == null ? null : s.trim(); } private static Map buildSuccessResponse(Map data) { Map result = new HashMap<>(); result.put("code", 0); result.put("msg", "success"); result.put("data", data != null ? data : new HashMap<>()); return result; } private static Map rrExceptionResponse(ASException e) { Map err = new HashMap<>(); err.put("code", e.getCode()); err.put("msg", e.getMessage()); err.put("data", new HashMap<>()); return err; } }