记得上下班打卡 | git大法好,push需谨慎

Commit 7f8a9858 authored by wangyifan's avatar wangyifan

草莓护照-用户端 绑定护照、首页

parent 6017d1aa
package com.liquidnet.service.adam.dto.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
@Data
@ApiModel("草莓护照-编号参数")
public class AdamCaomeiPassportNoParam {
@NotBlank(message = "护照编码不能为空")
@ApiModelProperty(value = "护照实体编号(扫码或手输)", required = true)
private String passportNo;
}
package com.liquidnet.service.adam.dto.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.liquidnet.commons.lang.util.DateUtil;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
@Data
@ApiModel("草莓护照-货架徽章项(含认领状态与交互提示)")
public class AdamCaomeiPassportBadgeShelfItemVo {
@ApiModelProperty("徽章ID")
private String badgeId;
@ApiModelProperty("名称")
private String name;
@ApiModelProperty("图标")
private String icon;
@ApiModelProperty("类型 1护照 2演出 3特殊")
private Integer type;
@ApiModelProperty("关联演出ID(演出纪念徽章)")
private String performanceId;
@ApiModelProperty("是否已认领")
private boolean claimed;
@ApiModelProperty("认领时间(未认领为空)")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DateUtil.DATE_FULL_STR)
private Date claimedAt;
@ApiModelProperty("是否可认领(护照徽章未发放完全,或演出徽章有票未领)")
private boolean claimable;
}
package com.liquidnet.service.adam.dto.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
@ApiModel("草莓护照-C端首页聚合")
public class AdamCaomeiPassportHomeVo {
@ApiModelProperty("个人信息卡片")
private AdamCaomeiPassportUserCardVo userCard;
@ApiModelProperty("已认领徽章(全部获得记录,用于网格墙)")
private List<AdamCaomeiPassportUserClaimedBadgeVo> claimedBadges;
@ApiModelProperty("全部上架徽章(扁平列表,前端按类型分组展示)")
private List<AdamCaomeiPassportBadgeShelfItemVo> allBadges;
}
package com.liquidnet.service.adam.dto.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("草莓护照-首页个人信息卡片")
public class AdamCaomeiPassportUserCardVo {
@ApiModelProperty("头像")
private String avatar;
@ApiModelProperty("昵称")
private String nickname;
@ApiModelProperty("是否已实名认证")
private boolean realNameVerified;
@ApiModelProperty("护照编号(未绑定时为空)")
private String passportNo;
@ApiModelProperty("是否已绑定实体护照")
private boolean passportBound;
}
package com.liquidnet.service.adam.dto.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.liquidnet.commons.lang.util.DateUtil;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
@Data
@ApiModel("草莓护照-已认领徽章(墙)")
public class AdamCaomeiPassportUserClaimedBadgeVo {
@ApiModelProperty("徽章ID")
private String badgeId;
@ApiModelProperty("名称")
private String name;
@ApiModelProperty("图标")
private String icon;
@ApiModelProperty("类型 1护照类型徽章 2演出类型徽章 3特殊徽章")
private Integer type;
@ApiModelProperty("获得时间")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DateUtil.DATE_FULL_STR)
private Date claimedAt;
@ApiModelProperty("获取途径: 1-绑定护照自动发放, 2-购票自动发放, 3-补签审核通过, 4-现场管理员手动发放")
private Integer source;
}
package com.liquidnet.service.adam.service;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiPassportHomeVo;
import com.liquidnet.service.base.ResponseDto;
/**
* 草莓护照(用户端)
*/
public interface IAdamCaomeiPassportUserService {
/**
* 绑定实体护照:校验编号有效性、是否可绑;通过则写入并发放已上架护照纪念徽章(type=1)
*/
ResponseDto<String> bindPassport(String passportNo);
/**
* 护照首页:个人信息、实名状态、已认领墙、按类型分组的全部上架徽章
*/
ResponseDto<AdamCaomeiPassportHomeVo> getPassportHome();
}
......@@ -9,6 +9,7 @@ public class AdamCaomeiPassportUserBadgeDto {
private String badgeId;
private String badgeName;
private String icon;
private Integer type;
private Date claimedAt;
private Integer source;
}
......@@ -5,6 +5,7 @@ import com.liquidnet.service.adam.dto.AdamCaomeiBadgeClaimCountDto;
import com.liquidnet.service.adam.dto.AdamCaomeiBadgeClaimUserDto;
import com.liquidnet.service.adam.entity.AdamCaomeiBadge;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
......@@ -47,4 +48,25 @@ public interface AdamCaomeiBadgeMapper extends BaseMapper<AdamCaomeiBadge> {
"where ub.user_id = #{uid} and ub.source = 1 and b.type = 1"
})
int deletePassportTypeBadgesByUid(@Param("uid") String uid);
/**
* 绑定护照成功后,为已上架的「护照纪念徽章」(type=1) 批量发放 source=1(与解绑删除规则对称)
*/
@Insert({
"insert ignore into adam_caomei_user_badge (user_id, badge_id, source, created_at) ",
"select #{userId}, b.badge_id, 1, now() ",
"from adam_caomei_badge b ",
"where b.type = 1 and b.display_status = 1"
})
int insertPassportBindingBadgesForUser(@Param("userId") String userId);
/**
* 根据身份证号查询用户已支付的演出ID列表
*/
@Select({
"select distinct performance_id ",
"from kylin_order_ticket_entities ",
"where enter_id_code = #{idCard} and is_payment = 1"
})
List<String> selectPaidPerformanceIdsByIdCard(@Param("idCard") String idCard);
}
......@@ -5,6 +5,7 @@ import com.liquidnet.service.adam.dto.AdamCaomeiPassportListDto;
import com.liquidnet.service.adam.dto.AdamCaomeiPassportUserBadgeDto;
import com.liquidnet.service.adam.entity.AdamCaomeiPassport;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import java.util.List;
......@@ -17,4 +18,10 @@ public interface AdamCaomeiPassportMapper extends BaseMapper<AdamCaomeiPassport>
@Param("bindStatus") Integer bindStatus);
List<AdamCaomeiPassportUserBadgeDto> selectUserBadgesByUid(@Param("uid") String uid);
/**
* 原子操作:仅当护照状态为 0(未绑定)时,更新为 1(已绑定)
*/
@Update("update adam_caomei_passport set status = 1, user_id = #{userId}, bound_at = now(), updated_at = now() where passport_no = #{passportNo} and status = 0")
int bindPassportAtomic(@Param("passportNo") String passportNo, @Param("userId") String userId);
}
......@@ -64,6 +64,7 @@
ub.badge_id AS badgeId,
IFNULL(b.name, '') AS badgeName,
IFNULL(b.icon, '') AS icon,
IFNULL(b.type, 0) AS type,
ub.created_at AS claimedAt,
ub.source AS source
FROM adam_caomei_user_badge ub
......
package com.liquidnet.service.adam.controller;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.liquidnet.service.adam.dto.param.AdamCaomeiPassportNoParam;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiPassportHomeVo;
import com.liquidnet.service.adam.service.IAdamCaomeiPassportUserService;
import com.liquidnet.service.base.ResponseDto;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@ApiSupport(order = 10045)
@Api(tags = "草莓护照(用户端)")
@Slf4j
@Validated
@RestController
@RequestMapping("caomei/passport")
public class AdamCaomeiPassportUserController {
@Autowired
private IAdamCaomeiPassportUserService adamCaomeiPassportUserService;
@ApiOperationSupport(order = 1)
@ApiOperation("绑定实体护照(内含编号有效性、是否可绑等校验)")
@PostMapping("bind")
public ResponseDto<String> bind(@Valid @RequestBody AdamCaomeiPassportNoParam param) {
return adamCaomeiPassportUserService.bindPassport(param.getPassportNo());
}
@ApiOperationSupport(order = 2)
@ApiOperation("护照首页聚合数据")
@GetMapping("home")
public ResponseDto<AdamCaomeiPassportHomeVo> home() {
return adamCaomeiPassportUserService.getPassportHome();
}
}
package com.liquidnet.service.adam.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.liquidnet.commons.lang.util.CurrentUtil;
import com.liquidnet.service.adam.dto.AdamCaomeiPassportUserBadgeDto;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiPassportBadgeShelfItemVo;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiPassportHomeVo;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiPassportUserCardVo;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiPassportUserClaimedBadgeVo;
import com.liquidnet.service.adam.dto.vo.AdamRealInfoVo;
import com.liquidnet.service.adam.dto.vo.AdamUserInfoVo;
import com.liquidnet.service.adam.entity.AdamCaomeiBadge;
import com.liquidnet.service.adam.entity.AdamCaomeiPassport;
import com.liquidnet.service.adam.mapper.AdamCaomeiBadgeMapper;
import com.liquidnet.service.adam.mapper.AdamCaomeiPassportMapper;
import com.liquidnet.service.adam.service.AdamRdmService;
import com.liquidnet.service.adam.service.IAdamCaomeiPassportUserService;
import com.liquidnet.service.base.ErrorMapping;
import com.liquidnet.service.base.ResponseDto;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
@Slf4j
@Service
public class AdamCaomeiPassportUserServiceImpl implements IAdamCaomeiPassportUserService {
@Autowired
private AdamCaomeiPassportMapper adamCaomeiPassportMapper;
@Autowired
private AdamCaomeiBadgeMapper adamCaomeiBadgeMapper;
@Autowired
private AdamRdmService adamRdmService;
@Override
@Transactional(rollbackFor = Exception.class)
public ResponseDto<String> bindPassport(String passportNo) {
String uid = CurrentUtil.getCurrentUid();
// 1. 先校验护照信息是否满足绑定条件
AdamCaomeiPassport passport = adamCaomeiPassportMapper.selectOne(
Wrappers.lambdaQuery(AdamCaomeiPassport.class)
.eq(AdamCaomeiPassport::getPassportNo, passportNo.trim()));
if (passport == null) {
log.error("[bindPassport] 未查询到护照信息, passportSn: {}", passportNo);
return ResponseDto.failure(ErrorMapping.get("10600"));
}
Integer st = passport.getStatus();
if (st != null && st == 2) {
log.error("[bindPassport] 护照已作废, passportSn: {}", passportNo);
return ResponseDto.failure(ErrorMapping.get("10601"));
}
if (st != null && st == 1) {
if (uid.equals(StringUtils.trimToEmpty(passport.getUserId()))) {
return ResponseDto.failure(ErrorMapping.get("10606")); // 提示无需重复绑定
}
log.error("[bindPassport] 护照已被其他账号绑定, passportSn: {}", passportNo);
return ResponseDto.failure(ErrorMapping.get("10602"));
}
// 2. 校验当前用户是否已经绑定过其他护照
AdamCaomeiPassport userExistsPassport = findBoundPassportForUser(uid);
if (userExistsPassport != null && !passportNo.equals(userExistsPassport.getPassportNo())) {
log.error("[bindPassport] 已绑定护照, uid: {}, existsPassportSn: {}, newPassportSn: {}", uid, userExistsPassport.getPassportNo(), passportNo);
return ResponseDto.failure(ErrorMapping.get("10603"));
}
// 3. 尝试原子更新绑定状态 (仅当 status = 0 时才能更新成功)
int updatedRows = adamCaomeiPassportMapper.bindPassportAtomic(passportNo.trim(), uid);
if (updatedRows > 0) {
// 4. 绑定成功后,自动发放已上架的“护照纪念徽章”(type=1)
adamCaomeiBadgeMapper.insertPassportBindingBadgesForUser(uid);
log.info("[bindPassport] 护照绑定成功, uid: {}, passportSn: {}", uid, passportNo);
return ResponseDto.success(passportNo);
}
// 5. 如果原子性修改未成功,说明在并发情况下被别人抢先绑定了
log.error("[bindPassport] 并发绑定失败,护照已被抢占, passportSn: {}", passportNo);
return ResponseDto.failure(ErrorMapping.get("10602"));
}
@Override
public ResponseDto<AdamCaomeiPassportHomeVo> getPassportHome() {
String uid = CurrentUtil.getCurrentUid();
log.info("[getPassportHome] 开始获取护照首页数据, uid: {}", uid);
// 1. 初始化返回对象
AdamCaomeiPassportHomeVo home = new AdamCaomeiPassportHomeVo();
AdamCaomeiPassportUserCardVo card = new AdamCaomeiPassportUserCardVo();
home.setUserCard(card);
// 2. 获取用户基本信息(头像、昵称)
AdamUserInfoVo userInfo = adamRdmService.getUserInfoVoByUid(uid);
if (userInfo != null) {
card.setAvatar(StringUtils.defaultString(userInfo.getAvatar()));
card.setNickname(StringUtils.defaultString(userInfo.getNickname()));
} else {
card.setAvatar("");
card.setNickname("");
}
// 3. 获取用户实名认证信息
AdamRealInfoVo real = adamRdmService.getRealInfoVoByUidPlain(uid);
String idCard = null;
boolean isRealNameVerified = false;
if (real != null && real.getState() != null && real.getState() == 1 && real.getType() == 1) {
isRealNameVerified = true;
idCard = real.getIdCard();
}
card.setRealNameVerified(isRealNameVerified);
log.info("[getPassportHome] 实名认证状态, uid: {}, 是否已实名: {}, 是否有身份证号: {}", uid, isRealNameVerified, StringUtils.isNotBlank(idCard));
// 4. 获取用户当前绑定的护照信息
AdamCaomeiPassport bound = findBoundPassportForUser(uid);
boolean isPassportBound = bound != null;
if (isPassportBound) {
card.setPassportBound(true);
card.setPassportNo(bound.getPassportNo());
} else {
card.setPassportBound(false);
card.setPassportNo("");
}
log.info("[getPassportHome] 护照绑定状态, uid: {}, 是否已绑定: {}, 护照编号: {}", uid, isPassportBound, card.getPassportNo());
// 5. 查询用户购买过的演出ID列表 (用于判断演出徽章是否可认领)
final List<String> paidPerformanceIds = StringUtils.isNotBlank(idCard)
? adamCaomeiBadgeMapper.selectPaidPerformanceIdsByIdCard(idCard)
: new ArrayList<>();
log.info("[getPassportHome] 用户已支付的演出订单数量, uid: {}, 数量: {}", uid, paidPerformanceIds.size());
// 6. 查询用户已认领的所有徽章记录 (用于展示徽章墙)
List<AdamCaomeiPassportUserBadgeDto> rows = adamCaomeiPassportMapper.selectUserBadgesByUid(uid);
List<AdamCaomeiPassportUserClaimedBadgeVo> claimed = rows.stream().map(r -> {
AdamCaomeiPassportUserClaimedBadgeVo v = new AdamCaomeiPassportUserClaimedBadgeVo();
v.setBadgeId(r.getBadgeId());
v.setName(StringUtils.defaultString(r.getBadgeName()));
v.setIcon(StringUtils.defaultString(r.getIcon()));
v.setType(r.getType());
v.setClaimedAt(r.getClaimedAt());
v.setSource(r.getSource());
return v;
}).collect(Collectors.toList());
home.setClaimedBadges(claimed);
log.info("[getPassportHome] 用户已认领的徽章数量, uid: {}, 数量: {}", uid, claimed.size());
// 转换为 Map 方便后续匹配货架上的徽章是否已认领
Map<String, AdamCaomeiPassportUserBadgeDto> claimedMap = rows.stream()
.filter(r -> StringUtils.isNotBlank(r.getBadgeId()))
.collect(Collectors.toMap(AdamCaomeiPassportUserBadgeDto::getBadgeId, Function.identity(), (a, b) -> a));
// 7. 查询所有已上架的徽章配置,并按类型升序、排序值降序、ID降序排序
List<AdamCaomeiBadge> published = adamCaomeiBadgeMapper.selectList(
Wrappers.lambdaQuery(AdamCaomeiBadge.class)
.eq(AdamCaomeiBadge::getDisplayStatus, 1)
.orderByAsc(AdamCaomeiBadge::getType)
.orderByDesc(AdamCaomeiBadge::getSort)
.orderByDesc(AdamCaomeiBadge::getMid)
);
log.info("[getPassportHome] 系统已上架的徽章数量, uid: {}, 数量: {}", uid, published.size());
// 8. 组装全部上架徽章列表 (扁平结构,前端按 type 筛选展示)
List<AdamCaomeiPassportBadgeShelfItemVo> allBadges = published.stream()
.map(b -> toShelfItem(b, claimedMap, isPassportBound, paidPerformanceIds))
.collect(Collectors.toList());
home.setAllBadges(allBadges);
log.info("[getPassportHome] 获取护照首页数据成功, uid: {}", uid);
return ResponseDto.success(home);
}
private static AdamCaomeiPassportBadgeShelfItemVo toShelfItem(AdamCaomeiBadge b, Map<String, AdamCaomeiPassportUserBadgeDto> claimedMap, boolean isPassportBound, List<String> paidPerformanceIds) {
AdamCaomeiPassportBadgeShelfItemVo v = new AdamCaomeiPassportBadgeShelfItemVo();
v.setBadgeId(b.getBadgeId());
v.setName(StringUtils.defaultString(b.getName()));
v.setIcon(StringUtils.defaultString(b.getIcon()));
v.setType(b.getType());
v.setPerformanceId(StringUtils.defaultString(b.getPerformanceId()));
// 判断当前徽章是否已认领
AdamCaomeiPassportUserBadgeDto got = claimedMap.get(b.getBadgeId());
boolean claimed = got != null;
v.setClaimed(claimed);
v.setClaimedAt(claimed ? got.getClaimedAt() : null);
int type = b.getType() == null ? 0 : b.getType();
// 针对未认领的徽章,根据类型判断是否可认领 (claimable)
if (!claimed) {
if (type == 1) {
// 护照纪念徽章:只要绑定了护照,就可认领(通常是绑定时漏发或后来新上架的)
v.setClaimable(isPassportBound);
} else if (type == 2) {
// 演出纪念徽章:只要有对应演出的已支付购票记录,就可认领
v.setClaimable(paidPerformanceIds != null && paidPerformanceIds.contains(b.getPerformanceId()));
} else if (type == 3) {
// 特殊徽章:不可自助领取,需要提示用户
v.setClaimable(false);
}
} else {
// 已认领的徽章,可认领状态置为 false
v.setClaimable(false);
}
return v;
}
private AdamCaomeiPassport findBoundPassportForUser(String uid) {
if (StringUtils.isBlank(uid)) {
return null;
}
return adamCaomeiPassportMapper.selectOne(
Wrappers.lambdaQuery(AdamCaomeiPassport.class)
.eq(AdamCaomeiPassport::getUserId, uid)
.eq(AdamCaomeiPassport::getStatus, 1)
.last("limit 1")
);
}
}
......@@ -86,6 +86,15 @@
10504=\u4F1A\u5458\u8BA2\u5355\u56DE\u8C03\u5904\u7406\u5931\u8D25\uFF0C\u4F1A\u5458\u4EF7\u683C\u4FE1\u606F\u4E0D\u5B58\u5728
10505=\u4F1A\u5458\u8BA2\u5355\u56DE\u8C03\u5904\u7406\u5F02\u5E38
# \u8349\u8393\u62A4\u7167\uFF08C\u7AEF\u7ED1\u5B9A/\u9996\u9875\uFF09
10600=\u62A4\u7167\u7F16\u53F7\u65E0\u6548\u6216\u4E0D\u5B58\u5728
10601=\u8BE5\u62A4\u7167\u5DF2\u4F5C\u5E9F
10602=\u8BE5\u62A4\u7167\u5DF2\u88AB\u5176\u4ED6\u8D26\u53F7\u7ED1\u5B9A
10603=\u60A8\u5DF2\u7ED1\u5B9A\u5176\u4ED6\u62A4\u7167\uFF0C\u65E0\u6CD5\u91CD\u590D\u7ED1\u5B9A
10604=\u62A4\u7167\u7F16\u53F7\u4E0D\u80FD\u4E3A\u7A7A
10605=\u62A4\u7167\u7ED1\u5B9A\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5
10606=\u8BE5\u62A4\u7167\u5DF2\u4E0E\u60A8\u7ED1\u5B9A\uFF0C\u65E0\u9700\u91CD\u590D\u64CD\u4F5C
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment