Commit 796589c357ae146a6ccb16d729da9bbebb27460a

Authored by shaofan
1 parent a417d65d

新增极光推送

... ... @@ -85,6 +85,13 @@
85 85 <scope>runtime</scope>
86 86 </dependency>
87 87  
  88 + <!-- 极光推送 -->
  89 + <dependency>
  90 + <groupId>cn.jpush.api</groupId>
  91 + <artifactId>jpush-client</artifactId>
  92 + <version>3.6.6</version>
  93 + </dependency>
  94 +
88 95 <!-- Test -->
89 96 <dependency>
90 97 <groupId>org.springframework.boot</groupId>
... ...
src/main/java/com/diligrp/rider/config/JPushConfig.java 0 → 100644
  1 +package com.diligrp.rider.config;
  2 +
  3 +import cn.jpush.api.JPushClient;
  4 +import lombok.RequiredArgsConstructor;
  5 +import lombok.extern.slf4j.Slf4j;
  6 +import org.springframework.context.annotation.Bean;
  7 +import org.springframework.context.annotation.Configuration;
  8 +
  9 +/**
  10 + * JPushClient Bean 装配
  11 + *
  12 + * 即使 jpush.enabled=false 也会创建一个 client(避免依赖注入失败),
  13 + * 调用时由 JPushService 检查 enabled 开关决定是否真的发起推送。
  14 + */
  15 +@Slf4j
  16 +@Configuration
  17 +@RequiredArgsConstructor
  18 +public class JPushConfig {
  19 +
  20 + private final JPushProperties properties;
  21 +
  22 + @Bean
  23 + public JPushClient jPushClient() {
  24 + if (!properties.isEnabled()) {
  25 + log.warn("JPush 未启用(jpush.enabled=false),将创建占位 client,所有推送会被跳过");
  26 + }
  27 + // 即便未启用,也用占位值构造,避免 NPE
  28 + String masterSecret = properties.getMasterSecret() != null ? properties.getMasterSecret() : "placeholder";
  29 + String appKey = properties.getAppKey() != null ? properties.getAppKey() : "placeholder";
  30 + return new JPushClient(masterSecret, appKey);
  31 + }
  32 +}
... ...
src/main/java/com/diligrp/rider/config/JPushProperties.java 0 → 100644
  1 +package com.diligrp.rider.config;
  2 +
  3 +import lombok.Data;
  4 +import org.springframework.boot.context.properties.ConfigurationProperties;
  5 +import org.springframework.context.annotation.Configuration;
  6 +
  7 +/**
  8 + * 极光推送配置
  9 + */
  10 +@Data
  11 +@Configuration
  12 +@ConfigurationProperties(prefix = "jpush")
  13 +public class JPushProperties {
  14 +
  15 + /** 是否启用 JPush;false 时所有推送调用静默跳过 */
  16 + private boolean enabled = false;
  17 +
  18 + /** AppKey */
  19 + private String appKey;
  20 +
  21 + /** MasterSecret */
  22 + private String masterSecret;
  23 +
  24 + /** iOS APNs 是否生产环境 */
  25 + private boolean apnsProduction = false;
  26 +
  27 + /** 离线消息保留秒数 */
  28 + private int timeToLive = 86400;
  29 +}
... ...
src/main/java/com/diligrp/rider/controller/RiderDeviceController.java 0 → 100644
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.result.Result;
  4 +import com.diligrp.rider.dto.DeviceBindDTO;
  5 +import com.diligrp.rider.service.RiderDeviceService;
  6 +import jakarta.servlet.http.HttpServletRequest;
  7 +import jakarta.validation.Valid;
  8 +import lombok.RequiredArgsConstructor;
  9 +import org.springframework.web.bind.annotation.PostMapping;
  10 +import org.springframework.web.bind.annotation.RequestBody;
  11 +import org.springframework.web.bind.annotation.RequestMapping;
  12 +import org.springframework.web.bind.annotation.RequestParam;
  13 +import org.springframework.web.bind.annotation.RestController;
  14 +
  15 +/**
  16 + * 骑手端推送设备绑定接口
  17 + */
  18 +@RestController
  19 +@RequestMapping("/api/rider/device")
  20 +@RequiredArgsConstructor
  21 +public class RiderDeviceController {
  22 +
  23 + private final RiderDeviceService deviceService;
  24 +
  25 + /**
  26 + * 登录后绑定 JPush registrationId
  27 + */
  28 + @PostMapping("/bind")
  29 + public Result<Void> bind(@Valid @RequestBody DeviceBindDTO dto, HttpServletRequest request) {
  30 + Long riderId = (Long) request.getAttribute("riderId");
  31 + deviceService.bind(riderId, dto);
  32 + return Result.success();
  33 + }
  34 +
  35 + /**
  36 + * 退出登录时解绑
  37 + */
  38 + @PostMapping("/unbind")
  39 + public Result<Void> unbind(@RequestParam String registrationId, HttpServletRequest request) {
  40 + Long riderId = (Long) request.getAttribute("riderId");
  41 + deviceService.unbind(riderId, registrationId);
  42 + return Result.success();
  43 + }
  44 +}
... ...
src/main/java/com/diligrp/rider/dto/DeviceBindDTO.java 0 → 100644
  1 +package com.diligrp.rider.dto;
  2 +
  3 +import jakarta.validation.constraints.NotBlank;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +/**
  8 + * 骑手端设备绑定参数
  9 + */
  10 +@Data
  11 +public class DeviceBindDTO {
  12 +
  13 + /** JPush 注册ID */
  14 + @NotBlank(message = "registrationId 不能为空")
  15 + private String registrationId;
  16 +
  17 + /** 平台:1=Android 2=iOS */
  18 + @NotNull(message = "platform 不能为空")
  19 + private Integer platform;
  20 +
  21 + /** 设备信息(可选) */
  22 + private String deviceInfo;
  23 +
  24 + /** App 版本(可选) */
  25 + private String appVersion;
  26 +}
... ...
src/main/java/com/diligrp/rider/entity/RiderDevice.java 0 → 100644
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.IdType;
  4 +import com.baomidou.mybatisplus.annotation.TableId;
  5 +import com.baomidou.mybatisplus.annotation.TableName;
  6 +import lombok.Data;
  7 +
  8 +/**
  9 + * 骑手设备推送绑定表
  10 + */
  11 +@Data
  12 +@TableName("rider_device")
  13 +public class RiderDevice {
  14 +
  15 + @TableId(type = IdType.AUTO)
  16 + private Long id;
  17 +
  18 + /** 骑手ID */
  19 + private Long riderId;
  20 +
  21 + /** JPush 设备注册ID */
  22 + private String registrationId;
  23 +
  24 + /** 平台:1=Android 2=iOS */
  25 + private Integer platform;
  26 +
  27 + /** 设备信息(型号/系统版本) */
  28 + private String deviceInfo;
  29 +
  30 + /** App 版本 */
  31 + private String appVersion;
  32 +
  33 + /** 状态:0=已解绑 1=正常 */
  34 + private Integer status;
  35 +
  36 + /** 最近活跃时间 */
  37 + private Long lastActiveTime;
  38 +
  39 + private Long createTime;
  40 +
  41 + private Long updateTime;
  42 +}
... ...
src/main/java/com/diligrp/rider/mapper/RiderDeviceMapper.java 0 → 100644
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.RiderDevice;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface RiderDeviceMapper extends BaseMapper<RiderDevice> {
  9 +}
... ...
src/main/java/com/diligrp/rider/service/JPushService.java 0 → 100644
  1 +package com.diligrp.rider.service;
  2 +
  3 +/**
  4 + * 极光推送服务
  5 + *
  6 + * 推送目标统一使用 alias = riderId 字符串;
  7 + * App 端登录后须调用 JPushFlutter.setAlias(riderId) 完成绑定。
  8 + */
  9 +public interface JPushService {
  10 +
  11 + /**
  12 + * 推送给单个骑手
  13 + *
  14 + * @param riderId 骑手ID(作为 JPush alias)
  15 + * @param title 通知标题
  16 + * @param content 通知正文
  17 + * @param bizType 业务类型(透传到 App,用于点击跳转)
  18 + * @param bizId 业务ID(如订单ID)
  19 + */
  20 + void pushToRider(Long riderId, String title, String content, String bizType, Long bizId);
  21 +
  22 + /**
  23 + * 按城市广播(按 tag = "city_<cityId>",App 端登录时按所属城市设置 tag)
  24 + */
  25 + void pushToCity(Long cityId, String title, String content, String bizType);
  26 +}
... ...
src/main/java/com/diligrp/rider/service/RiderDeviceService.java 0 → 100644
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.dto.DeviceBindDTO;
  4 +
  5 +/**
  6 + * 骑手设备绑定服务
  7 + */
  8 +public interface RiderDeviceService {
  9 +
  10 + /**
  11 + * 登录后绑定设备(同 registrationId 已存在则刷新归属与活跃时间)
  12 + */
  13 + void bind(Long riderId, DeviceBindDTO dto);
  14 +
  15 + /**
  16 + * 退出登录时解绑(按 registrationId)
  17 + */
  18 + void unbind(Long riderId, String registrationId);
  19 +}
... ...
src/main/java/com/diligrp/rider/service/impl/AdminMessageServiceImpl.java
... ... @@ -10,6 +10,7 @@ import com.diligrp.rider.mapper.RiderMapper;
10 10 import com.diligrp.rider.mapper.RiderMessageMapper;
11 11 import com.diligrp.rider.mapper.RiderMessageTemplateMapper;
12 12 import com.diligrp.rider.service.AdminMessageService;
  13 +import com.diligrp.rider.service.JPushService;
13 14 import lombok.RequiredArgsConstructor;
14 15 import lombok.extern.slf4j.Slf4j;
15 16 import org.springframework.stereotype.Service;
... ... @@ -28,6 +29,7 @@ public class AdminMessageServiceImpl implements AdminMessageService {
28 29 private final RiderMessageMapper messageMapper;
29 30 private final RiderMessageTemplateMapper templateMapper;
30 31 private final RiderMapper riderMapper;
  32 + private final JPushService jPushService;
31 33  
32 34 private static final int PAGE_SIZE = 20;
33 35  
... ... @@ -37,6 +39,15 @@ public class AdminMessageServiceImpl implements AdminMessageService {
37 39 ensureCitySelected(cityId);
38 40 ensureRiderInCity(cityId, riderId);
39 41  
  42 + long messageId = insertMessage(cityId, riderId, type, title, content, bizType, bizId);
  43 + log.info("发送消息成功,riderId={} messageId={}", riderId, messageId);
  44 +
  45 + // 写库后触发推送(管理员手动单发:总是推送)
  46 + jPushService.pushToRider(riderId, title, content, bizType, bizId);
  47 + }
  48 +
  49 + private long insertMessage(Long cityId, Long riderId, Integer type, String title,
  50 + String content, String bizType, Long bizId) {
40 51 RiderMessage message = new RiderMessage();
41 52 message.setCityId(cityId);
42 53 message.setRiderId(riderId);
... ... @@ -48,9 +59,8 @@ public class AdminMessageServiceImpl implements AdminMessageService {
48 59 message.setIsRead(0);
49 60 message.setCreateTime(System.currentTimeMillis());
50 61 message.setReadTime(0L);
51   -
52 62 messageMapper.insert(message);
53   - log.info("发送消息成功,riderId={} messageId={}", riderId, message.getId());
  63 + return message.getId();
54 64 }
55 65  
56 66 @Override
... ... @@ -96,6 +106,9 @@ public class AdminMessageServiceImpl implements AdminMessageService {
96 106 }
97 107  
98 108 log.info("群发消息成功,cityId={} 骑手数={}", cityId, riders.size());
  109 +
  110 + // 写库后按城市 tag 触发广播推送
  111 + jPushService.pushToCity(cityId, title, content, "broadcast");
99 112 }
100 113  
101 114 @Override
... ... @@ -164,9 +177,15 @@ public class AdminMessageServiceImpl implements AdminMessageService {
164 177 String title = replacePlaceholders(template.getTitleTemplate(), params);
165 178 String content = replacePlaceholders(template.getContentTemplate(), params);
166 179  
167   - // 发送消息
  180 + // 模板的 is_push=1 时同步推送,否则仅入库
168 181 Long bizId = params.get("bizId") != null ? Long.valueOf(params.get("bizId").toString()) : 0L;
169   - sendToRider(cityId, riderId, template.getType(), title, content, bizType, bizId);
  182 + if (Integer.valueOf(1).equals(template.getIsPush())) {
  183 + sendToRider(cityId, riderId, template.getType(), title, content, bizType, bizId);
  184 + } else {
  185 + ensureCitySelected(cityId);
  186 + ensureRiderInCity(cityId, riderId);
  187 + insertMessage(cityId, riderId, template.getType(), title, content, bizType, bizId);
  188 + }
170 189 }
171 190  
172 191 /**
... ...
src/main/java/com/diligrp/rider/service/impl/JPushServiceImpl.java 0 → 100644
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import cn.jiguang.common.resp.APIConnectionException;
  4 +import cn.jiguang.common.resp.APIRequestException;
  5 +import cn.jpush.api.JPushClient;
  6 +import cn.jpush.api.push.PushResult;
  7 +import cn.jpush.api.push.model.Options;
  8 +import cn.jpush.api.push.model.Platform;
  9 +import cn.jpush.api.push.model.PushPayload;
  10 +import cn.jpush.api.push.model.audience.Audience;
  11 +import cn.jpush.api.push.model.notification.AndroidNotification;
  12 +import cn.jpush.api.push.model.notification.IosNotification;
  13 +import cn.jpush.api.push.model.notification.Notification;
  14 +import com.diligrp.rider.config.JPushProperties;
  15 +import com.diligrp.rider.service.JPushService;
  16 +import lombok.RequiredArgsConstructor;
  17 +import lombok.extern.slf4j.Slf4j;
  18 +import org.springframework.stereotype.Service;
  19 +
  20 +@Slf4j
  21 +@Service
  22 +@RequiredArgsConstructor
  23 +public class JPushServiceImpl implements JPushService {
  24 +
  25 + private final JPushClient jPushClient;
  26 + private final JPushProperties properties;
  27 +
  28 + @Override
  29 + public void pushToRider(Long riderId, String title, String content, String bizType, Long bizId) {
  30 + if (!properties.isEnabled()) {
  31 + log.debug("JPush 未启用,跳过推送 riderId={} title={}", riderId, title);
  32 + return;
  33 + }
  34 + if (riderId == null || riderId <= 0) {
  35 + log.warn("riderId 无效,跳过推送 riderId={}", riderId);
  36 + return;
  37 + }
  38 + try {
  39 + PushPayload payload = buildPayload(
  40 + Audience.alias(String.valueOf(riderId)),
  41 + title, content, bizType, bizId);
  42 + PushResult result = jPushClient.sendPush(payload);
  43 + log.info("JPush 推送成功 riderId={} msgId={} sendno={}",
  44 + riderId, result.msg_id, result.sendno);
  45 + } catch (APIConnectionException e) {
  46 + log.error("JPush 网络异常 riderId={}", riderId, e);
  47 + } catch (APIRequestException e) {
  48 + log.error("JPush 业务异常 riderId={} status={} errorCode={} msg={}",
  49 + riderId, e.getStatus(), e.getErrorCode(), e.getErrorMessage());
  50 + } catch (Exception e) {
  51 + log.error("JPush 推送未知异常 riderId={}", riderId, e);
  52 + }
  53 + }
  54 +
  55 + @Override
  56 + public void pushToCity(Long cityId, String title, String content, String bizType) {
  57 + if (!properties.isEnabled()) {
  58 + log.debug("JPush 未启用,跳过广播 cityId={} title={}", cityId, title);
  59 + return;
  60 + }
  61 + if (cityId == null || cityId <= 0) {
  62 + log.warn("cityId 无效,跳过广播 cityId={}", cityId);
  63 + return;
  64 + }
  65 + try {
  66 + PushPayload payload = buildPayload(
  67 + Audience.tag("city_" + cityId),
  68 + title, content, bizType, 0L);
  69 + PushResult result = jPushClient.sendPush(payload);
  70 + log.info("JPush 城市广播成功 cityId={} msgId={}", cityId, result.msg_id);
  71 + } catch (APIConnectionException e) {
  72 + log.error("JPush 城市广播网络异常 cityId={}", cityId, e);
  73 + } catch (APIRequestException e) {
  74 + log.error("JPush 城市广播业务异常 cityId={} status={} errorCode={} msg={}",
  75 + cityId, e.getStatus(), e.getErrorCode(), e.getErrorMessage());
  76 + } catch (Exception e) {
  77 + log.error("JPush 城市广播未知异常 cityId={}", cityId, e);
  78 + }
  79 + }
  80 +
  81 + private PushPayload buildPayload(Audience audience, String title, String content,
  82 + String bizType, Long bizId) {
  83 + AndroidNotification.Builder androidBuilder = AndroidNotification.newBuilder()
  84 + .setAlert(content)
  85 + .setTitle(title);
  86 + IosNotification.Builder iosBuilder = IosNotification.newBuilder()
  87 + .setAlert(content)
  88 + .incrBadge(1)
  89 + .setSound("default");
  90 +
  91 + if (bizType != null && !bizType.isEmpty()) {
  92 + androidBuilder.addExtra("bizType", bizType);
  93 + iosBuilder.addExtra("bizType", bizType);
  94 + }
  95 + if (bizId != null) {
  96 + androidBuilder.addExtra("bizId", String.valueOf(bizId));
  97 + iosBuilder.addExtra("bizId", String.valueOf(bizId));
  98 + }
  99 +
  100 + return PushPayload.newBuilder()
  101 + .setPlatform(Platform.android_ios())
  102 + .setAudience(audience)
  103 + .setNotification(Notification.newBuilder()
  104 + .addPlatformNotification(androidBuilder.build())
  105 + .addPlatformNotification(iosBuilder.build())
  106 + .build())
  107 + .setOptions(Options.newBuilder()
  108 + .setApnsProduction(properties.isApnsProduction())
  109 + .setTimeToLive(properties.getTimeToLive())
  110 + .build())
  111 + .build();
  112 + }
  113 +}
... ...
src/main/java/com/diligrp/rider/service/impl/RiderDeviceServiceImpl.java 0 → 100644
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5 +import com.diligrp.rider.dto.DeviceBindDTO;
  6 +import com.diligrp.rider.entity.RiderDevice;
  7 +import com.diligrp.rider.mapper.RiderDeviceMapper;
  8 +import com.diligrp.rider.service.RiderDeviceService;
  9 +import lombok.RequiredArgsConstructor;
  10 +import lombok.extern.slf4j.Slf4j;
  11 +import org.springframework.stereotype.Service;
  12 +import org.springframework.transaction.annotation.Transactional;
  13 +
  14 +@Slf4j
  15 +@Service
  16 +@RequiredArgsConstructor
  17 +public class RiderDeviceServiceImpl implements RiderDeviceService {
  18 +
  19 + private final RiderDeviceMapper deviceMapper;
  20 +
  21 + @Override
  22 + @Transactional
  23 + public void bind(Long riderId, DeviceBindDTO dto) {
  24 + long now = System.currentTimeMillis();
  25 +
  26 + // 同一 registrationId 全局唯一:若已存在则覆盖归属
  27 + LambdaQueryWrapper<RiderDevice> wrapper = new LambdaQueryWrapper<>();
  28 + wrapper.eq(RiderDevice::getRegistrationId, dto.getRegistrationId());
  29 + RiderDevice existing = deviceMapper.selectOne(wrapper);
  30 +
  31 + if (existing != null) {
  32 + existing.setRiderId(riderId);
  33 + existing.setPlatform(dto.getPlatform());
  34 + existing.setDeviceInfo(dto.getDeviceInfo() == null ? "" : dto.getDeviceInfo());
  35 + existing.setAppVersion(dto.getAppVersion() == null ? "" : dto.getAppVersion());
  36 + existing.setStatus(1);
  37 + existing.setLastActiveTime(now);
  38 + existing.setUpdateTime(now);
  39 + deviceMapper.updateById(existing);
  40 + log.info("设备绑定刷新 riderId={} registrationId={}", riderId, dto.getRegistrationId());
  41 + return;
  42 + }
  43 +
  44 + RiderDevice device = new RiderDevice();
  45 + device.setRiderId(riderId);
  46 + device.setRegistrationId(dto.getRegistrationId());
  47 + device.setPlatform(dto.getPlatform());
  48 + device.setDeviceInfo(dto.getDeviceInfo() == null ? "" : dto.getDeviceInfo());
  49 + device.setAppVersion(dto.getAppVersion() == null ? "" : dto.getAppVersion());
  50 + device.setStatus(1);
  51 + device.setLastActiveTime(now);
  52 + device.setCreateTime(now);
  53 + device.setUpdateTime(now);
  54 + deviceMapper.insert(device);
  55 + log.info("设备绑定新增 riderId={} registrationId={}", riderId, dto.getRegistrationId());
  56 + }
  57 +
  58 + @Override
  59 + @Transactional
  60 + public void unbind(Long riderId, String registrationId) {
  61 + if (registrationId == null || registrationId.isEmpty()) {
  62 + return;
  63 + }
  64 + long now = System.currentTimeMillis();
  65 + LambdaUpdateWrapper<RiderDevice> wrapper = new LambdaUpdateWrapper<>();
  66 + wrapper.eq(RiderDevice::getRegistrationId, registrationId)
  67 + .eq(RiderDevice::getRiderId, riderId)
  68 + .set(RiderDevice::getStatus, 0)
  69 + .set(RiderDevice::getUpdateTime, now);
  70 + deviceMapper.update(null, wrapper);
  71 + log.info("设备解绑 riderId={} registrationId={}", riderId, registrationId);
  72 + }
  73 +}
... ...
src/main/resources/application.yml
... ... @@ -31,6 +31,13 @@ jwt:
31 31 secret: diligrp-rider-secret-key-2024-please-change-this
32 32 expire: 604800 # 7天,单位秒
33 33  
  34 +jpush:
  35 + enabled: false # 未配置 AppKey 前置 false,避免启动时报错
  36 + app-key: your-jpush-app-key
  37 + master-secret: your-jpush-master-secret
  38 + apns-production: false # iOS APNs:true=生产 false=开发
  39 + time-to-live: 86400 # 离线消息保留秒数(默认 1 天)
  40 +
34 41 logging:
35 42 level:
36 43 com.diligrp.rider: debug
... ...
src/main/resources/schema.sql
... ... @@ -581,4 +581,21 @@ INSERT INTO `rider_message_template` (`biz_type`, `type`, `title_template`, `con
581 581 ('order_cancelled', 1, '订单取消', '订单 #{orderNo} 已被取消', 1),
582 582 ('system_notice', 2, '系统通知', '#{content}', 1),
583 583 ('level_upgrade', 2, '等级提升', '恭喜您,等级已提升至 #{levelName}', 1),
584   -('balance_change', 2, '余额变动', '您的余额发生变动,当前余额:#{balance} 元', 0);
585 584 \ No newline at end of file
  585 +('balance_change', 2, '余额变动', '您的余额发生变动,当前余额:#{balance} 元', 0);
  586 +
  587 +-- 骑手设备推送绑定表(极光推送)
  588 +CREATE TABLE `rider_device` (
  589 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  590 + `rider_id` BIGINT UNSIGNED NOT NULL COMMENT '骑手ID',
  591 + `registration_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'JPush 设备注册ID',
  592 + `platform` TINYINT NOT NULL DEFAULT 0 COMMENT '平台:1=Android 2=iOS',
  593 + `device_info` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '设备信息(型号/系统版本)',
  594 + `app_version` VARCHAR(32) NOT NULL DEFAULT '' COMMENT 'App 版本',
  595 + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=已解绑 1=正常',
  596 + `last_active_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '最近活跃时间',
  597 + `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  598 + `update_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  599 + PRIMARY KEY (`id`),
  600 + UNIQUE KEY `uk_registration_id` (`registration_id`),
  601 + KEY `idx_rider_status` (`rider_id`, `status`)
  602 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='骑手设备推送绑定表';
586 603 \ No newline at end of file
... ...