bug修改

This commit is contained in:
周鹏 committed 2026-09-05 23:44:01 +08:00
1 parent e046c67f64
commit 1024045a92
11 files changed
+104 -56

No files matched your search

+8 -6
View File
@@ -1,14 +1,15 @@
-- 航空账号(管理员开通:手机号+密码+有效期+设备唯一绑定)
CREATE TABLE IF NOT EXISTS `member_airline_account` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
`mobile` varchar(11) NOT NULL COMMENT '手机号',
`mobile` varchar(11) NOT NULL COMMENT '手机号(同一手机号可在不同渠道各有一个账号)',
`channel` varchar(8) NOT NULL DEFAULT 'CZ' COMMENT '品牌渠道(CZ南航/MU东航/HU海航)',
`password` varchar(100) NOT NULL DEFAULT '' COMMENT '密码(BCrypt)',
`start_time` datetime NOT NULL COMMENT '生效开始时间',
`end_time` datetime NOT NULL COMMENT '生效结束时间',
`device_id` varchar(128) NULL DEFAULT NULL COMMENT '手机唯一识别号(首次登录绑定;重装/重启应由客户端保持不变)',
`status` tinyint NOT NULL DEFAULT 0 COMMENT '状态(0开启 1关闭)',
`remark` varchar(500) NULL DEFAULT NULL COMMENT '备注',
`user_id` bigint NULL DEFAULT NULL COMMENT '关联会员用户编号 member_user.id',
`user_id` bigint NULL DEFAULT NULL COMMENT '关联会员用户编号 member_user.id(已废弃,登录不再创建会员用户)',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
`creator` varchar(64) NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
@@ -16,8 +17,8 @@ CREATE TABLE IF NOT EXISTS `member_airline_account` (
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_mobile` (`mobile`, `deleted`, `tenant_id`) USING BTREE,
UNIQUE KEY `uk_device_id` (`device_id`, `deleted`, `tenant_id`) USING BTREE,
UNIQUE KEY `uk_mobile_channel` (`mobile`, `channel`, `deleted`, `tenant_id`) USING BTREE,
UNIQUE KEY `uk_channel_device` (`channel`, `device_id`, `deleted`, `tenant_id`) USING BTREE,
KEY `idx_end_time` (`end_time`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='航空账号';
@@ -40,8 +41,9 @@ ON DUPLICATE KEY UPDATE `permission` = VALUES(`permission`);
-- 手机号:13800138000 密码:123456 有效期约一年 未绑定设备
-- 密码哈希为 BCrypt(123456)
INSERT INTO `member_airline_account`
(`mobile`, `password`, `start_time`, `end_time`, `device_id`, `status`, `remark`, `user_id`, `tenant_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
(`mobile`, `channel`, `password`, `start_time`, `end_time`, `device_id`, `status`, `remark`, `user_id`, `tenant_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT '13800138000',
'CZ',
'$2a$10$PplOV8yXcQo9xc32V8vwTuIMbMKgNtjXf3lu6l18LUTOy9YGDdAY6',
NOW() - INTERVAL 1 DAY,
NOW() + INTERVAL 365 DAY,
@@ -58,5 +60,5 @@ SELECT '13800138000',
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `member_airline_account`
WHERE `mobile` = '13800138000' AND `deleted` = b'0' AND `tenant_id` = 1
WHERE `mobile` = '13800138000' AND `channel` = 'CZ' AND `deleted` = b'0' AND `tenant_id` = 1
);
@@ -0,0 +1,20 @@
-- 航空账号增加品牌渠道(channel):CZ 南航 / MU 东航 / HU 海航
-- 目的:三个 App 使用同一手机号分别登录,互不干扰。
-- 唯一键调整:
-- uk_mobile (mobile, deleted, tenant_id) -> 废弃
-- uk_device_id (device_id, deleted, tenant_id) -> 废弃
-- uk_mobile_channel (mobile, channel, deleted, tenant_id) -> 同一手机号每渠道一个账号
-- uk_channel_device (channel, device_id, deleted, tenant_id) -> 同一设备每渠道绑定一个账号
ALTER TABLE `member_airline_account`
ADD COLUMN `channel` varchar(8) NOT NULL DEFAULT 'CZ' COMMENT '品牌渠道(CZ南航/MU东航/HU海航)' AFTER `mobile`;
-- 存量数据归属渠道默认回填为 CZ(南航),如实际属于东航/海航请手工执行:
-- UPDATE `member_airline_account` SET `channel` = 'MU' WHERE ...;
-- UPDATE `member_airline_account` SET `channel` = 'HU' WHERE ...;
ALTER TABLE `member_airline_account`
DROP INDEX `uk_mobile`,
DROP INDEX `uk_device_id`,
ADD UNIQUE KEY `uk_mobile_channel` (`mobile`, `channel`, `deleted`, `tenant_id`),
ADD UNIQUE KEY `uk_channel_device` (`channel`, `device_id`, `deleted`, `tenant_id`);
@@ -7,6 +7,7 @@ import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@@ -20,6 +21,11 @@ public class MemberAirlineAccountBaseVO {
@Length(min = 11, max = 11, message = "手机号长度为 11 位")
private String mobile;
@Schema(description = "品牌渠道(CZ南航/MU东航/HU海航)", requiredMode = Schema.RequiredMode.REQUIRED, example = "CZ")
@NotEmpty(message = "品牌渠道不能为空")
@Pattern(regexp = "CZ|MU|HU", message = "品牌渠道仅支持 CZ(南航)/ MU(东航)/ HU(海航)")
private String channel;
@Schema(description = "生效开始时间", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "开始时间不能为空")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@@ -20,6 +20,9 @@ public class MemberAirlineAccountPageReqVO extends PageParam {
@Schema(description = "手机号", example = "13800138000")
private String mobile;
@Schema(description = "品牌渠道(CZ南航/MU东航/HU海航)", example = "CZ")
private String channel;
@Schema(description = "状态(0开启 1关闭)", example = "0")
private Integer status;
@@ -15,6 +15,9 @@ public class MemberAirlineAccountRespVO {
@Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13800138000")
private String mobile;
@Schema(description = "品牌渠道(CZ南航/MU东航/HU海航)", requiredMode = Schema.RequiredMode.REQUIRED, example = "CZ")
private String channel;
@Schema(description = "生效开始时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime startTime;
@@ -6,6 +6,7 @@ import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
@Schema(description = "用户 APP - 航空账号登录 Request VO")
@Data
@@ -16,6 +17,11 @@ public class AppAuthAirlineLoginReqVO {
@Mobile
private String mobile;
@Schema(description = "品牌渠道", requiredMode = Schema.RequiredMode.REQUIRED, example = "CZ")
@NotEmpty(message = "品牌渠道不能为空")
@Pattern(regexp = "CZ|MU|HU", message = "品牌渠道仅支持 CZ(南航)/ MU(东航)/ HU(海航)")
private String channel;
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@NotEmpty(message = "密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
@@ -23,8 +23,10 @@ public class MemberAirlineAccountDO extends TenantBaseDO {
@TableId
private Long id;
/** 手机号 */
/** 手机号(同一手机号可在不同渠道各有一个账号) */
private String mobile;
/** 品牌渠道:CZ 南航 / MU 东航 / HU 海航 */
private String channel;
/** 密码(BCrypt) */
private String password;
/** 生效开始时间 */
@@ -13,22 +13,23 @@ public interface MemberAirlineAccountMapper extends BaseMapperX<MemberAirlineAcc
default PageResult<MemberAirlineAccountDO> selectPage(MemberAirlineAccountPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<MemberAirlineAccountDO>()
.likeIfPresent(MemberAirlineAccountDO::getMobile, reqVO.getMobile())
.eqIfPresent(MemberAirlineAccountDO::getChannel, reqVO.getChannel())
.eqIfPresent(MemberAirlineAccountDO::getStatus, reqVO.getStatus())
.likeIfPresent(MemberAirlineAccountDO::getDeviceId, reqVO.getDeviceId())
.betweenIfPresent(MemberAirlineAccountDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(MemberAirlineAccountDO::getId));
}
default MemberAirlineAccountDO selectByMobile(String mobile) {
return selectOne(MemberAirlineAccountDO::getMobile, mobile);
default MemberAirlineAccountDO selectByMobileAndChannel(String mobile, String channel) {
return selectOne(new LambdaQueryWrapperX<MemberAirlineAccountDO>()
.eq(MemberAirlineAccountDO::getMobile, mobile)
.eq(MemberAirlineAccountDO::getChannel, channel));
}
default MemberAirlineAccountDO selectByDeviceId(String deviceId) {
return selectOne(MemberAirlineAccountDO::getDeviceId, deviceId);
}
default MemberAirlineAccountDO selectByUserId(Long userId) {
return selectOne(MemberAirlineAccountDO::getUserId, userId);
default MemberAirlineAccountDO selectByDeviceIdAndChannel(String deviceId, String channel) {
return selectOne(new LambdaQueryWrapperX<MemberAirlineAccountDO>()
.eq(MemberAirlineAccountDO::getDeviceId, deviceId)
.eq(MemberAirlineAccountDO::getChannel, channel));
}
}
@@ -26,18 +26,17 @@ public interface MemberAirlineAccountService {
/** 管理端解绑设备,允许换机重新登录绑定 */
void unbindDevice(Long id);
/** 登录成功后回写关联的会员用户编号 */
void updateAirlineAccountUserId(Long id, Long userId);
/**
* 登录校验:密码、有效期、设备绑定。
* 首次登录绑定 deviceId;成功后返回账号(可能已写入 deviceId / userId)。
* 手机号与设备号均按渠道(channel)隔离:同一手机号/同一设备可在 CZ/MU/HU 各绑定一个账号。
* 首次登录绑定 deviceId;成功后返回账号(可能已写入 deviceId)。
*/
MemberAirlineAccountDO authenticateAndBindDevice(String mobile, String password, String deviceId);
MemberAirlineAccountDO authenticateAndBindDevice(String mobile, String password, String deviceId, String channel);
/**
* 会话校验:已登录用户打开 App 时检查账号是否禁用/过期/设备不匹配。
* 注意:accountId 为登录 Token 中的 userId,即航空账号自身编号(不再是 member_user 编号)。
*/
void validateAirlineSession(Long userId, String deviceId);
void validateAirlineSession(Long accountId, String deviceId);
}
@@ -33,7 +33,7 @@ public class MemberAirlineAccountServiceImpl implements MemberAirlineAccountServ
@Override
public Long createAirlineAccount(MemberAirlineAccountCreateReqVO createReqVO) {
validateTimeRange(createReqVO.getStartTime(), createReqVO.getEndTime());
validateMobileUnique(null, createReqVO.getMobile());
validateMobileUnique(null, createReqVO.getMobile(), createReqVO.getChannel());
MemberAirlineAccountDO account = BeanUtils.toBean(createReqVO, MemberAirlineAccountDO.class);
account.setPassword(passwordEncoder.encode(createReqVO.getPassword()));
@@ -46,7 +46,7 @@ public class MemberAirlineAccountServiceImpl implements MemberAirlineAccountServ
public void updateAirlineAccount(MemberAirlineAccountUpdateReqVO updateReqVO) {
validateExists(updateReqVO.getId());
validateTimeRange(updateReqVO.getStartTime(), updateReqVO.getEndTime());
validateMobileUnique(updateReqVO.getId(), updateReqVO.getMobile());
validateMobileUnique(updateReqVO.getId(), updateReqVO.getMobile(), updateReqVO.getChannel());
MemberAirlineAccountDO updateObj = BeanUtils.toBean(updateReqVO, MemberAirlineAccountDO.class);
if (StrUtil.isBlank(updateReqVO.getPassword())) {
@@ -83,18 +83,10 @@ public class MemberAirlineAccountServiceImpl implements MemberAirlineAccountServ
.set(MemberAirlineAccountDO::getDeviceId, null));
}
@Override
public void updateAirlineAccountUserId(Long id, Long userId) {
MemberAirlineAccountDO update = new MemberAirlineAccountDO();
update.setId(id);
update.setUserId(userId);
airlineAccountMapper.updateById(update);
}
@Override
@Transactional(rollbackFor = Exception.class)
public MemberAirlineAccountDO authenticateAndBindDevice(String mobile, String password, String deviceId) {
MemberAirlineAccountDO account = airlineAccountMapper.selectByMobile(mobile);
public MemberAirlineAccountDO authenticateAndBindDevice(String mobile, String password, String deviceId, String channel) {
MemberAirlineAccountDO account = airlineAccountMapper.selectByMobileAndChannel(mobile, channel);
if (account == null) {
throw exception(AIRLINE_ACCOUNT_BAD_CREDENTIALS);
}
@@ -114,8 +106,8 @@ public class MemberAirlineAccountServiceImpl implements MemberAirlineAccountServ
String boundDeviceId = account.getDeviceId();
if (StrUtil.isBlank(boundDeviceId)) {
// 首次绑定:设备不能已被其他账号占用
MemberAirlineAccountDO byDevice = airlineAccountMapper.selectByDeviceId(deviceId);
// 首次绑定:同渠道下设备不能已被其他账号占用(不同渠道互不影响)
MemberAirlineAccountDO byDevice = airlineAccountMapper.selectByDeviceIdAndChannel(deviceId, channel);
if (byDevice != null && !Objects.equals(byDevice.getId(), account.getId())) {
throw exception(AIRLINE_ACCOUNT_DEVICE_USED);
}
@@ -131,11 +123,12 @@ public class MemberAirlineAccountServiceImpl implements MemberAirlineAccountServ
}
@Override
public void validateAirlineSession(Long userId, String deviceId) {
if (userId == null) {
public void validateAirlineSession(Long accountId, String deviceId) {
if (accountId == null) {
throw exception(AIRLINE_ACCOUNT_NOT_EXISTS);
}
MemberAirlineAccountDO account = airlineAccountMapper.selectByUserId(userId);
// 登录签发的 Token 中 userId 即航空账号自身编号
MemberAirlineAccountDO account = airlineAccountMapper.selectById(accountId);
if (account == null) {
throw exception(AIRLINE_ACCOUNT_NOT_EXISTS);
}
@@ -164,8 +157,9 @@ public class MemberAirlineAccountServiceImpl implements MemberAirlineAccountServ
return account;
}
private void validateMobileUnique(Long id, String mobile) {
MemberAirlineAccountDO exist = airlineAccountMapper.selectByMobile(mobile);
private void validateMobileUnique(Long id, String mobile, String channel) {
// 同一手机号允许在不同渠道各建一个账号(CZ/MU/HU 互不干扰)
MemberAirlineAccountDO exist = airlineAccountMapper.selectByMobileAndChannel(mobile, channel);
if (exist == null) {
return;
}
@@ -411,25 +411,37 @@ public class MemberAuthServiceImpl implements MemberAuthService {
@Override
@Transactional(rollbackFor = Exception.class)
public AppAuthLoginRespVO airlineLogin(AppAuthAirlineLoginReqVO reqVO) {
// 1. 校验航空账号:密码、有效期、设备绑定
// 1. 校验航空账号:渠道、密码、有效期、设备绑定(手机号/设备号均按渠道隔离)
MemberAirlineAccountDO account = airlineAccountService.authenticateAndBindDevice(
reqVO.getMobile(), reqVO.getPassword(), reqVO.getDeviceId());
reqVO.getMobile(), reqVO.getPassword(), reqVO.getDeviceId(), reqVO.getChannel());
// 2. 同步会员用户(不存在则创建),并覆盖密码保持一致
MemberUserDO user = userService.createUserIfAbsent(reqVO.getMobile(), getClientIP(), getTerminal());
Assert.notNull(user, "获取用户失败,结果为空");
userService.updateUserPassword(user.getId(), reqVO.getPassword());
// 2. 直接以航空账号身份签发 Token(不创建、不关联 member_user,避免污染会员表)
return createTokenAfterAirlineLoginSuccess(account, reqVO.getMobile());
}
// 3. 回写关联会员编号
if (!Objects.equals(account.getUserId(), user.getId())) {
MemberAirlineAccountDO update = new MemberAirlineAccountDO();
update.setId(account.getId());
update.setUserId(user.getId());
airlineAccountService.updateAirlineAccountUserId(account.getId(), user.getId());
}
// 4. 签发 Token(与密码登录一致)
return createTokenAfterLoginSuccess(user, reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE, null);
/**
* 航空账号专用:签发 Token + 记录登录日志。
* 注意:不能复用 {@link #createTokenAfterLoginSuccess} 和 {@link #createLoginLog},
* 它们在成功时会更新 member_user(状态校验、最后登录时间),而航空账号没有对应的会员用户。
* Token 的 userId 直接使用航空账号自身编号。
*/
private AppAuthLoginRespVO createTokenAfterAirlineLoginSuccess(MemberAirlineAccountDO account, String mobile) {
// 插入登录日志(不更新 member_user 的最后登录信息)
LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO();
reqDTO.setLogType(LoginLogTypeEnum.LOGIN_MOBILE.getType());
reqDTO.setTraceId(TracerUtils.getTraceId());
reqDTO.setUserId(account.getId());
reqDTO.setUserType(getUserType().getValue());
reqDTO.setUsername(mobile);
reqDTO.setUserAgent(ServletUtils.getUserAgent());
reqDTO.setUserIp(getClientIP());
reqDTO.setResult(LoginResultEnum.SUCCESS.getResult());
loginLogApi.createLoginLog(reqDTO);
// 创建 Token 令牌(userId 使用航空账号自身编号)
OAuth2AccessTokenRespDTO accessTokenRespDTO = oauth2TokenApi.createAccessToken(new OAuth2AccessTokenCreateReqDTO()
.setUserId(account.getId()).setUserType(getUserType().getValue())
.setClientId(OAuth2ClientConstants.CLIENT_ID_DEFAULT));
return AuthConvert.INSTANCE.convert(accessTokenRespDTO, null);
}
@Override