From 800b7650df2898ac9855d460434de4f4e5fc37bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E9=B9=8F?= <812968653@qq.com> Date: Mon, 31 Aug 2026 07:35:24 +0800 Subject: [PATCH] =?UTF-8?q?bug=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/mysql/member/member_airline_account.sql | 62 +++++++ .../module/member/enums/MemberErrorCode.java | 11 ++ .../MemberAirlineAccountController.java | 96 +++++++++++ .../vo/MemberAirlineAccountBaseVO.java | 40 +++++ .../vo/MemberAirlineAccountCreateReqVO.java | 22 +++ .../vo/MemberAirlineAccountPageReqVO.java | 33 ++++ .../vo/MemberAirlineAccountRespVO.java | 45 +++++ .../vo/MemberAirlineAccountUpdateReqVO.java | 25 +++ .../app/auth/AppAuthController.java | 7 + .../app/auth/vo/AppAuthAirlineLoginReqVO.java | 30 ++++ .../airline/MemberAirlineAccountDO.java | 47 ++++++ .../airline/MemberAirlineAccountMapper.java | 30 ++++ .../airline/MemberAirlineAccountService.java | 38 +++++ .../MemberAirlineAccountServiceImpl.java | 157 ++++++++++++++++++ .../service/auth/MemberAuthService.java | 5 + .../service/auth/MemberAuthServiceImpl.java | 28 ++++ .../service/user/MemberUserService.java | 8 + .../service/user/MemberUserServiceImpl.java | 7 + 18 files changed, 691 insertions(+) create mode 100644 sql/mysql/member/member_airline_account.sql create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/MemberAirlineAccountController.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/vo/MemberAirlineAccountBaseVO.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/vo/MemberAirlineAccountCreateReqVO.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/vo/MemberAirlineAccountPageReqVO.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/vo/MemberAirlineAccountRespVO.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/vo/MemberAirlineAccountUpdateReqVO.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/vo/AppAuthAirlineLoginReqVO.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/airline/MemberAirlineAccountDO.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/airline/MemberAirlineAccountMapper.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/airline/MemberAirlineAccountService.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/airline/MemberAirlineAccountServiceImpl.java diff --git a/sql/mysql/member/member_airline_account.sql b/sql/mysql/member/member_airline_account.sql new file mode 100644 index 0000000..f186882 --- /dev/null +++ b/sql/mysql/member/member_airline_account.sql @@ -0,0 +1,62 @@ +-- 航空账号(管理员开通:手机号+密码+有效期+设备唯一绑定) +CREATE TABLE IF NOT EXISTS `member_airline_account` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', + `mobile` varchar(11) NOT NULL COMMENT '手机号', + `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', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + `creator` varchar(64) NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) NULL DEFAULT '' COMMENT '更新者', + `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, + KEY `idx_end_time` (`end_time`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='航空账号'; + +-- 菜单:航空账号管理(挂在会员中心 2262 下) +-- 若 id 冲突,请在管理后台「菜单管理」手动新建,或调整 id +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) +VALUES (5800, '航空账号', '', 2, 15, 2262, 'airline-account', 'ep:iphone', 'member/airlineAccount/index', 'MemberAirlineAccount', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0') +ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), `component` = VALUES(`component`); + +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) +VALUES +(5801, '航空账号查询', 'member:airline-account:query', 3, 1, 5800, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'), +(5802, '航空账号创建', 'member:airline-account:create', 3, 2, 5800, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'), +(5803, '航空账号更新', 'member:airline-account:update', 3, 3, 5800, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'), +(5804, '航空账号删除', 'member:airline-account:delete', 3, 4, 5800, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'), +(5805, '航空账号解绑设备', 'member:airline-account:unbind', 3, 5, 5800, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0') +ON DUPLICATE KEY UPDATE `permission` = VALUES(`permission`); + +-- 测试账号(App 登录) +-- 手机号: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`) +SELECT '13800138000', + '$2a$10$PplOV8yXcQo9xc32V8vwTuIMbMKgNtjXf3lu6l18LUTOy9YGDdAY6', + NOW() - INTERVAL 1 DAY, + NOW() + INTERVAL 365 DAY, + NULL, + 0, + '本地联调测试账号', + NULL, + 1, + '1', + NOW(), + '1', + NOW(), + b'0' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `member_airline_account` + WHERE `mobile` = '13800138000' AND `deleted` = b'0' AND `tenant_id` = 1 +); diff --git a/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberErrorCode.java b/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberErrorCode.java index 013d824..8e57b7a 100644 --- a/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberErrorCode.java +++ b/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberErrorCode.java @@ -91,4 +91,15 @@ public interface MemberErrorCode { ErrorCode CHAT_USER_COUNT_EXCEED = new ErrorCode(1_004_018_000, "当天免费聊天人数已用完"); ErrorCode CHAT_MSG_COUNT_EXCEED = new ErrorCode(1_004_018_001, "当天免费聊天次数已用完"); ErrorCode CHAT_TOKEN_CREATE_FAILED = new ErrorCode(1_004_018_002, "聊天登录凭证签发失败,请稍后重试"); + + //========== 航空账号 1-004-019-000 ========== + ErrorCode AIRLINE_ACCOUNT_NOT_EXISTS = new ErrorCode(1_004_019_000, "航空账号不存在"); + ErrorCode AIRLINE_ACCOUNT_MOBILE_EXISTS = new ErrorCode(1_004_019_001, "该手机号已存在航空账号"); + ErrorCode AIRLINE_ACCOUNT_DISABLED = new ErrorCode(1_004_019_002, "航空账号已禁用"); + ErrorCode AIRLINE_ACCOUNT_NOT_STARTED = new ErrorCode(1_004_019_003, "航空账号尚未生效"); + ErrorCode AIRLINE_ACCOUNT_EXPIRED = new ErrorCode(1_004_019_004, "航空账号已到期,无法登录"); + ErrorCode AIRLINE_ACCOUNT_DEVICE_BOUND = new ErrorCode(1_004_019_005, "该账号已绑定其他手机,请联系管理员解绑"); + ErrorCode AIRLINE_ACCOUNT_DEVICE_USED = new ErrorCode(1_004_019_006, "该手机已绑定其他航空账号"); + ErrorCode AIRLINE_ACCOUNT_TIME_INVALID = new ErrorCode(1_004_019_007, "结束时间必须晚于开始时间"); + ErrorCode AIRLINE_ACCOUNT_BAD_CREDENTIALS = new ErrorCode(1_004_019_008, "航空账号手机号或密码不正确"); } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/MemberAirlineAccountController.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/MemberAirlineAccountController.java new file mode 100644 index 0000000..82df45f --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/MemberAirlineAccountController.java @@ -0,0 +1,96 @@ +package cn.iocoder.yudao.module.member.controller.admin.airline; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.member.controller.admin.airline.vo.*; +import cn.iocoder.yudao.module.member.dal.dataobject.airline.MemberAirlineAccountDO; +import cn.iocoder.yudao.module.member.service.airline.MemberAirlineAccountService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 航空账号") +@RestController +@RequestMapping("/member/airline-account") +@Validated +public class MemberAirlineAccountController { + + @Resource + private MemberAirlineAccountService airlineAccountService; + + @PostMapping("/create") + @Operation(summary = "创建航空账号") + @PreAuthorize("@ss.hasPermission('member:airline-account:create')") + public CommonResult createAirlineAccount(@Valid @RequestBody MemberAirlineAccountCreateReqVO createReqVO) { + return success(airlineAccountService.createAirlineAccount(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新航空账号") + @PreAuthorize("@ss.hasPermission('member:airline-account:update')") + public CommonResult updateAirlineAccount(@Valid @RequestBody MemberAirlineAccountUpdateReqVO updateReqVO) { + airlineAccountService.updateAirlineAccount(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除航空账号") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('member:airline-account:delete')") + public CommonResult deleteAirlineAccount(@RequestParam("id") Long id) { + airlineAccountService.deleteAirlineAccount(id); + return success(true); + } + + @PutMapping("/unbind-device") + @Operation(summary = "解绑设备(换机后需重新登录绑定)") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('member:airline-account:unbind')") + public CommonResult unbindDevice(@RequestParam("id") Long id) { + airlineAccountService.unbindDevice(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得航空账号") + @Parameter(name = "id", description = "编号", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('member:airline-account:query')") + public CommonResult getAirlineAccount(@RequestParam("id") Long id) { + return success(convert(airlineAccountService.getAirlineAccount(id))); + } + + @GetMapping("/page") + @Operation(summary = "获得航空账号分页") + @PreAuthorize("@ss.hasPermission('member:airline-account:query')") + public CommonResult> getAirlineAccountPage( + @Valid MemberAirlineAccountPageReqVO pageReqVO) { + PageResult pageResult = airlineAccountService.getAirlineAccountPage(pageReqVO); + List list = pageResult.getList().stream() + .map(this::convert).collect(Collectors.toList()); + return success(new PageResult<>(list, pageResult.getTotal())); + } + + private MemberAirlineAccountRespVO convert(MemberAirlineAccountDO account) { + if (account == null) { + return null; + } + MemberAirlineAccountRespVO vo = BeanUtils.toBean(account, MemberAirlineAccountRespVO.class); + LocalDateTime now = LocalDateTime.now(); + vo.setExpired(account.getEndTime() != null && now.isAfter(account.getEndTime())); + vo.setNotStarted(account.getStartTime() != null && now.isBefore(account.getStartTime())); + return vo; + } + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/vo/MemberAirlineAccountBaseVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/vo/MemberAirlineAccountBaseVO.java new file mode 100644 index 0000000..8f45fb4 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/vo/MemberAirlineAccountBaseVO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.member.controller.admin.airline.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Length; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 航空账号新增/修改 Base VO") +@Data +public class MemberAirlineAccountBaseVO { + + @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13800138000") + @NotEmpty(message = "手机号不能为空") + @Length(min = 11, max = 11, message = "手机号长度为 11 位") + private String mobile; + + @Schema(description = "生效开始时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "开始时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime startTime; + + @Schema(description = "生效结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "结束时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime endTime; + + @Schema(description = "状态(0开启 1关闭)", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "状态不能为空") + private Integer status; + + @Schema(description = "备注", example = "某某航司临时账号") + private String remark; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/vo/MemberAirlineAccountCreateReqVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/vo/MemberAirlineAccountCreateReqVO.java new file mode 100644 index 0000000..035b141 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/vo/MemberAirlineAccountCreateReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.member.controller.admin.airline.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - 航空账号创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberAirlineAccountCreateReqVO extends MemberAirlineAccountBaseVO { + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + @NotEmpty(message = "密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/vo/MemberAirlineAccountPageReqVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/vo/MemberAirlineAccountPageReqVO.java new file mode 100644 index 0000000..6ac8038 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/vo/MemberAirlineAccountPageReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.member.controller.admin.airline.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 航空账号分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberAirlineAccountPageReqVO extends PageParam { + + @Schema(description = "手机号", example = "13800138000") + private String mobile; + + @Schema(description = "状态(0开启 1关闭)", example = "0") + private Integer status; + + @Schema(description = "设备唯一识别号", example = "A1B2C3D4") + private String deviceId; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/vo/MemberAirlineAccountRespVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/vo/MemberAirlineAccountRespVO.java new file mode 100644 index 0000000..1ced489 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/vo/MemberAirlineAccountRespVO.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.member.controller.admin.airline.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 航空账号 Response VO") +@Data +public class MemberAirlineAccountRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13800138000") + private String mobile; + + @Schema(description = "生效开始时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime startTime; + + @Schema(description = "生效结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime endTime; + + @Schema(description = "设备唯一识别号", example = "A1B2C3D4") + private String deviceId; + + @Schema(description = "状态(0开启 1关闭)", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer status; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "关联会员用户编号") + private Long userId; + + @Schema(description = "是否已过期") + private Boolean expired; + + @Schema(description = "是否尚未生效") + private Boolean notStarted; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/vo/MemberAirlineAccountUpdateReqVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/vo/MemberAirlineAccountUpdateReqVO.java new file mode 100644 index 0000000..2f014f6 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/airline/vo/MemberAirlineAccountUpdateReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.member.controller.admin.airline.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 航空账号更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberAirlineAccountUpdateReqVO extends MemberAirlineAccountBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "密码(不传或空则不修改)", example = "123456") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.java index 338a4cd..bd7d7dd 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.java @@ -49,6 +49,13 @@ public class AppAuthController { return success(authService.login(reqVO)); } + @PostMapping("/airline-login") + @Operation(summary = "航空账号登录(手机 + 密码 + 设备唯一识别号)") + @PermitAll + public CommonResult airlineLogin(@RequestBody @Valid AppAuthAirlineLoginReqVO reqVO) { + return success(authService.airlineLogin(reqVO)); + } + @PostMapping("/logout") @Operation(summary = "登出系统") @PermitAll diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/vo/AppAuthAirlineLoginReqVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/vo/AppAuthAirlineLoginReqVO.java new file mode 100644 index 0000000..ea91ddee --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/vo/AppAuthAirlineLoginReqVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.member.controller.app.auth.vo; + +import cn.iocoder.yudao.framework.common.validation.Mobile; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotEmpty; + +@Schema(description = "用户 APP - 航空账号登录 Request VO") +@Data +public class AppAuthAirlineLoginReqVO { + + @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13800138000") + @NotEmpty(message = "手机号不能为空") + @Mobile + private String mobile; + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + @NotEmpty(message = "密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; + + @Schema(description = "手机唯一识别号(重装/重启后须保持不变)", requiredMode = Schema.RequiredMode.REQUIRED, + example = "android-id-or-keychain-uuid") + @NotEmpty(message = "设备唯一识别号不能为空") + @Length(min = 8, max = 128, message = "设备唯一识别号长度为 8-128 位") + private String deviceId; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/airline/MemberAirlineAccountDO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/airline/MemberAirlineAccountDO.java new file mode 100644 index 0000000..fd9a68a --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/airline/MemberAirlineAccountDO.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.member.dal.dataobject.airline; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 航空账号 DO + */ +@TableName("member_airline_account") +@KeySequence("member_airline_account_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberAirlineAccountDO extends TenantBaseDO { + + @TableId + private Long id; + /** 手机号 */ + private String mobile; + /** 密码(BCrypt) */ + private String password; + /** 生效开始时间 */ + private LocalDateTime startTime; + /** 生效结束时间 */ + private LocalDateTime endTime; + /** + * 手机唯一识别号。首次登录绑定;客户端需保证重装/重启后不变(如 Android ANDROID_ID、iOS Keychain UUID)。 + */ + private String deviceId; + /** + * 状态:0 开启 1 关闭,见 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + /** 备注 */ + private String remark; + /** 关联会员用户编号 */ + private Long userId; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/airline/MemberAirlineAccountMapper.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/airline/MemberAirlineAccountMapper.java new file mode 100644 index 0000000..fc36788 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/airline/MemberAirlineAccountMapper.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.member.dal.mysql.airline; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.member.controller.admin.airline.vo.MemberAirlineAccountPageReqVO; +import cn.iocoder.yudao.module.member.dal.dataobject.airline.MemberAirlineAccountDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface MemberAirlineAccountMapper extends BaseMapperX { + + default PageResult selectPage(MemberAirlineAccountPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(MemberAirlineAccountDO::getMobile, reqVO.getMobile()) + .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 selectByDeviceId(String deviceId) { + return selectOne(MemberAirlineAccountDO::getDeviceId, deviceId); + } + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/airline/MemberAirlineAccountService.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/airline/MemberAirlineAccountService.java new file mode 100644 index 0000000..421e793 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/airline/MemberAirlineAccountService.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.member.service.airline; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.member.controller.admin.airline.vo.MemberAirlineAccountCreateReqVO; +import cn.iocoder.yudao.module.member.controller.admin.airline.vo.MemberAirlineAccountPageReqVO; +import cn.iocoder.yudao.module.member.controller.admin.airline.vo.MemberAirlineAccountUpdateReqVO; +import cn.iocoder.yudao.module.member.dal.dataobject.airline.MemberAirlineAccountDO; + +import javax.validation.Valid; + +/** + * 航空账号 Service + */ +public interface MemberAirlineAccountService { + + Long createAirlineAccount(@Valid MemberAirlineAccountCreateReqVO createReqVO); + + void updateAirlineAccount(@Valid MemberAirlineAccountUpdateReqVO updateReqVO); + + void deleteAirlineAccount(Long id); + + MemberAirlineAccountDO getAirlineAccount(Long id); + + PageResult getAirlineAccountPage(MemberAirlineAccountPageReqVO pageReqVO); + + /** 管理端解绑设备,允许换机重新登录绑定 */ + void unbindDevice(Long id); + + /** 登录成功后回写关联的会员用户编号 */ + void updateAirlineAccountUserId(Long id, Long userId); + + /** + * 登录校验:密码、有效期、设备绑定。 + * 首次登录绑定 deviceId;成功后返回账号(可能已写入 deviceId / userId)。 + */ + MemberAirlineAccountDO authenticateAndBindDevice(String mobile, String password, String deviceId); + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/airline/MemberAirlineAccountServiceImpl.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/airline/MemberAirlineAccountServiceImpl.java new file mode 100644 index 0000000..541aada --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/airline/MemberAirlineAccountServiceImpl.java @@ -0,0 +1,157 @@ +package cn.iocoder.yudao.module.member.service.airline; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.member.controller.admin.airline.vo.MemberAirlineAccountCreateReqVO; +import cn.iocoder.yudao.module.member.controller.admin.airline.vo.MemberAirlineAccountPageReqVO; +import cn.iocoder.yudao.module.member.controller.admin.airline.vo.MemberAirlineAccountUpdateReqVO; +import cn.iocoder.yudao.module.member.dal.dataobject.airline.MemberAirlineAccountDO; +import cn.iocoder.yudao.module.member.dal.mysql.airline.MemberAirlineAccountMapper; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.member.enums.MemberErrorCode.*; + +@Service +@Validated +public class MemberAirlineAccountServiceImpl implements MemberAirlineAccountService { + + @Resource + private MemberAirlineAccountMapper airlineAccountMapper; + @Resource + private PasswordEncoder passwordEncoder; + + @Override + public Long createAirlineAccount(MemberAirlineAccountCreateReqVO createReqVO) { + validateTimeRange(createReqVO.getStartTime(), createReqVO.getEndTime()); + validateMobileUnique(null, createReqVO.getMobile()); + + MemberAirlineAccountDO account = BeanUtils.toBean(createReqVO, MemberAirlineAccountDO.class); + account.setPassword(passwordEncoder.encode(createReqVO.getPassword())); + account.setDeviceId(null); + airlineAccountMapper.insert(account); + return account.getId(); + } + + @Override + public void updateAirlineAccount(MemberAirlineAccountUpdateReqVO updateReqVO) { + validateExists(updateReqVO.getId()); + validateTimeRange(updateReqVO.getStartTime(), updateReqVO.getEndTime()); + validateMobileUnique(updateReqVO.getId(), updateReqVO.getMobile()); + + MemberAirlineAccountDO updateObj = BeanUtils.toBean(updateReqVO, MemberAirlineAccountDO.class); + if (StrUtil.isBlank(updateReqVO.getPassword())) { + updateObj.setPassword(null); // 不更新密码 + } else { + updateObj.setPassword(passwordEncoder.encode(updateReqVO.getPassword())); + } + // 不允许通过普通更新改 deviceId + updateObj.setDeviceId(null); + airlineAccountMapper.updateById(updateObj); + } + + @Override + public void deleteAirlineAccount(Long id) { + validateExists(id); + airlineAccountMapper.deleteById(id); + } + + @Override + public MemberAirlineAccountDO getAirlineAccount(Long id) { + return airlineAccountMapper.selectById(id); + } + + @Override + public PageResult getAirlineAccountPage(MemberAirlineAccountPageReqVO pageReqVO) { + return airlineAccountMapper.selectPage(pageReqVO); + } + + @Override + public void unbindDevice(Long id) { + validateExists(id); + airlineAccountMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() + .eq(MemberAirlineAccountDO::getId, id) + .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); + if (account == null) { + throw exception(AIRLINE_ACCOUNT_BAD_CREDENTIALS); + } + if (!passwordEncoder.matches(password, account.getPassword())) { + throw exception(AIRLINE_ACCOUNT_BAD_CREDENTIALS); + } + if (CommonStatusEnum.isDisable(account.getStatus())) { + throw exception(AIRLINE_ACCOUNT_DISABLED); + } + LocalDateTime now = LocalDateTime.now(); + if (now.isBefore(account.getStartTime())) { + throw exception(AIRLINE_ACCOUNT_NOT_STARTED); + } + if (now.isAfter(account.getEndTime())) { + throw exception(AIRLINE_ACCOUNT_EXPIRED); + } + + String boundDeviceId = account.getDeviceId(); + if (StrUtil.isBlank(boundDeviceId)) { + // 首次绑定:设备不能已被其他账号占用 + MemberAirlineAccountDO byDevice = airlineAccountMapper.selectByDeviceId(deviceId); + if (byDevice != null && !Objects.equals(byDevice.getId(), account.getId())) { + throw exception(AIRLINE_ACCOUNT_DEVICE_USED); + } + MemberAirlineAccountDO update = new MemberAirlineAccountDO(); + update.setId(account.getId()); + update.setDeviceId(deviceId); + airlineAccountMapper.updateById(update); + account.setDeviceId(deviceId); + } else if (!StrUtil.equals(boundDeviceId, deviceId)) { + throw exception(AIRLINE_ACCOUNT_DEVICE_BOUND); + } + return account; + } + + private MemberAirlineAccountDO validateExists(Long id) { + MemberAirlineAccountDO account = airlineAccountMapper.selectById(id); + if (account == null) { + throw exception(AIRLINE_ACCOUNT_NOT_EXISTS); + } + return account; + } + + private void validateMobileUnique(Long id, String mobile) { + MemberAirlineAccountDO exist = airlineAccountMapper.selectByMobile(mobile); + if (exist == null) { + return; + } + if (id == null || !Objects.equals(exist.getId(), id)) { + throw exception(AIRLINE_ACCOUNT_MOBILE_EXISTS); + } + } + + private void validateTimeRange(LocalDateTime startTime, LocalDateTime endTime) { + if (startTime != null && endTime != null && !endTime.isAfter(startTime)) { + throw exception(AIRLINE_ACCOUNT_TIME_INVALID); + } + } + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthService.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthService.java index bf051be..b0d9b58 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthService.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthService.java @@ -109,4 +109,9 @@ public interface MemberAuthService { * @return 聊天令牌 */ AppAuthLoginRespVO.ChatToken reissueChatToken(Long userId); + + /** + * 航空账号登录:校验密码、有效期、设备唯一绑定,并签发会员 Token + */ + AppAuthLoginRespVO airlineLogin(@Valid AppAuthAirlineLoginReqVO reqVO); } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java index c2f1be2..418f8f4 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java @@ -16,6 +16,8 @@ import cn.iocoder.yudao.module.member.controller.app.auth.vo.*; import cn.iocoder.yudao.module.member.convert.auth.AuthConvert; import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO; import cn.iocoder.yudao.module.member.dal.redis.auth.MemberAuthLoginCodeRedisDAO; +import cn.iocoder.yudao.module.member.dal.dataobject.airline.MemberAirlineAccountDO; +import cn.iocoder.yudao.module.member.service.airline.MemberAirlineAccountService; import cn.iocoder.yudao.module.member.service.user.MemberRecentCfgService; import cn.iocoder.yudao.module.member.service.user.MemberRecentTagService; import cn.iocoder.yudao.module.member.service.user.MemberUserService; @@ -78,6 +80,8 @@ public class MemberAuthServiceImpl implements MemberAuthService { private ChatApi chatApi; @Resource private MemberAuthLoginCodeRedisDAO authLoginCodeRedisDAO; + @Resource + private MemberAirlineAccountService airlineAccountService; @Override public AppAuthLoginRespVO login(AppAuthLoginReqVO reqVO) { @@ -404,6 +408,30 @@ public class MemberAuthServiceImpl implements MemberAuthService { return BeanUtils.toBean(loginRespVO, AppAuthLoginRespVO.ChatToken.class); } + @Override + @Transactional(rollbackFor = Exception.class) + public AppAuthLoginRespVO airlineLogin(AppAuthAirlineLoginReqVO reqVO) { + // 1. 校验航空账号:密码、有效期、设备绑定 + MemberAirlineAccountDO account = airlineAccountService.authenticateAndBindDevice( + reqVO.getMobile(), reqVO.getPassword(), reqVO.getDeviceId()); + + // 2. 同步会员用户(不存在则创建),并覆盖密码保持一致 + MemberUserDO user = userService.createUserIfAbsent(reqVO.getMobile(), getClientIP(), getTerminal()); + Assert.notNull(user, "获取用户失败,结果为空"); + userService.updateUserPassword(user.getId(), reqVO.getPassword()); + + // 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); + } + private void createLogoutLog(Long userId) { LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); reqDTO.setLogType(LoginLogTypeEnum.LOGOUT_SELF.getType()); diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserService.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserService.java index 2f9e3fc..da86c9c 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserService.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserService.java @@ -140,6 +140,14 @@ public interface MemberUserService { */ boolean isPasswordMatch(String rawPassword, String encodedPassword); + /** + * 直接覆盖用户密码(航空账号同步等内部场景) + * + * @param id 用户编号 + * @param password 明文密码 + */ + void updateUserPassword(Long id, String password); + /** * 【管理员】更新会员用户 * diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java index ad08305..d30f830 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java @@ -406,6 +406,13 @@ public class MemberUserServiceImpl implements MemberUserService { return passwordEncoder.matches(rawPassword, encodedPassword); } + @Override + public void updateUserPassword(Long id, String password) { + validateUserExists(id); + memberUserMapper.updateById(MemberUserDO.builder().id(id) + .password(encodePassword(password)).build()); + } + /** * 对密码进行加密 *