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

Commit a677fa25 authored by 姜秀龙's avatar 姜秀龙

Merge branch 'master' into dev-1.6-shouqianba

# Conflicts:
#	liquidnet-bus-common/liquidnet-common-service-base/src/main/java/com/liquidnet/service/base/constant/RedisKeyExpireConst.java
#	liquidnet-bus-service/liquidnet-service-goblin/liquidnet-service-goblin-impl/src/main/java/com/liquidnet/service/goblin/service/impl/GoblinFrontServiceImpl.java
parents 461ce778 f8932630
This source diff could not be displayed because it is too large. You can view the blob instead.
-- ----------------------------
-- 1. 护照实体表 (预先生成,供用户扫码绑定)
-- ----------------------------
CREATE TABLE `adam_caomei_passport` (
`mid` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`passport_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '护照编号 (全局唯一,用于生成二维码)',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '绑定状态: 0-未绑定, 1-已绑定, 2-已作废',
`user_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '绑定的用户ID',
`bound_at` datetime DEFAULT NULL COMMENT '首次绑定时间',
`unbound_at` datetime DEFAULT NULL COMMENT '最近一次解绑时间 (后台操作解绑时记录)',
`batch_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '生成/印刷批次号 (便于后期溯源管理)',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '后台运营备注',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`mid`),
UNIQUE KEY `uk_passport_no` (`passport_no`),
KEY `idx_user_id` (`user_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='草莓护照-实体编号表';
-- ----------------------------
-- 2. 徽章配置表 (运营后台配置,已移除 claimed_count,后台列表实时 COUNT 统计)
-- ----------------------------
CREATE TABLE `adam_caomei_badge` (
`mid` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`badge_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '徽章ID',
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '徽章名称',
`icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '徽章图标 (Emoji字符或图片URL)',
`type` tinyint(4) NOT NULL COMMENT '徽章类型: 1-护照纪念徽章, 2-演出纪念徽章, 3-特殊徽章',
`performance_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '关联演出ID (仅演出纪念徽章必填,其他类型为空)',
`display_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '上架状态: 0-下架(默认), 1-已发布',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`mid`),
KEY `idx_badge_id` (`badge_id`),
KEY `idx_type_status` (`type`,`display_status`),
KEY `idx_performance_id` (`performance_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='草莓护照-徽章配置表';
-- ----------------------------
-- 3. 用户徽章获得记录表 (支撑用户徽章墙与后台领取人数统计)
-- ----------------------------
CREATE TABLE `adam_caomei_user_badge` (
`mid` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '用户id',
`badge_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '徽章ID',
`source` tinyint(4) NOT NULL COMMENT '获取途径: 1-绑定护照自动发放, 2-购票自动发放, 3-补签审核通过, 4-现场管理员手动发放',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '获得时间',
PRIMARY KEY (`mid`),
UNIQUE KEY `uk_user_badge` (`user_id`,`badge_id`) COMMENT '联合唯一索引: 同一用户同一徽章只能获得一次',
KEY `idx_badge_id` (`badge_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='草莓护照-用户徽章获得记录表';
-- ----------------------------
-- 4. 徽章补签申请记录表 (支撑补签审核流程)
-- ----------------------------
CREATE TABLE `adam_caomei_badge_apply_record` (
`mid` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`apply_record_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '补签id',
`user_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '用户id',
`badge_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '申请补签的徽章ID',
`performance_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '关联的演出ID (冗余字段,便于后台筛选)',
`proof_image_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '订单截图/凭证图片URL',
`audit_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '审核状态: 0-待审核, 1-已通过, 2-已驳回',
`reject_reason` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '驳回理由 (驳回时必填,用户端可见)',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '申请时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '审核/更新时间',
PRIMARY KEY (`mid`),
KEY `idx_record_id`(`apply_record_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_audit_status` (`audit_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='草莓护照-徽章补签申请记录表';
-- 2026-04-13 新增排序字段
ALTER TABLE `adam_caomei_badge` ADD COLUMN `sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序值,数值越大越靠前' AFTER `display_status`;
-- 2026-04-16 新增分享文案字段
ALTER TABLE `adam_caomei_badge` ADD COLUMN `share_text` varchar(255) NOT NULL DEFAULT '' COMMENT '徽章分享文案' AFTER `sort`;
-- 2026-04-23 新增徽章副标题字段
ALTER TABLE `adam_caomei_badge` ADD COLUMN `sub_title` varchar(32) NOT NULL DEFAULT '' COMMENT '徽章副标题(最多20字)' AFTER `name`;
-- TODO 上线前需要运行:adam_caomei_passport_inserts_BATCH-CAOMEI-B1.sql
\ No newline at end of file
......@@ -20,6 +20,12 @@ public class AdamRedisConst {
public static final String INFO_THIRD_PARTY = PREFIX.concat("info:third_party:");
public static final String INFO_ENTERS = PREFIX.concat("info:enters:");
public static final String INFO_ADDRESSES = PREFIX.concat("info:addresses:");
public static final String INFO_CAOMEI_BADGE_PUBLISHED = PREFIX.concat("info:caomei:badge:published");
public static final String INFO_CAOMEI_BADGE_USER = PREFIX.concat("info:caomei:badge:user:");
/**
* 身份证号已支付演出ID列表(短缓存,缓解 kylin_order_ticket_entities 无身份证索引时的热点查询压力)
*/
public static final String INFO_CAOMEI_PAID_PERFORMANCE_IDS_BY_IDCARD = PREFIX.concat("info:caomei:paid_performance_ids:idcard:");
/**
* {adam:info:biz:{uid},List<com.liquidnet.service.adam.dto.vo.AdamUserBizAcctVo>}
*/
......@@ -36,6 +42,12 @@ public class AdamRedisConst {
*/
public static final String INFO_CERTIFICATION_JUNK = PREFIX.concat("info:certification_junk:");
/**
* 身份证号 -> 已绑定实名的 uid(用于快速判重:同一身份证只能实名一个账号)
* value: uid(String)
*/
public static final String INFO_REAL_NAME_UID_BY_IDCARD = PREFIX.concat("info:real_name:uid_by_idcard:");
public static final String INFO_MEMBER_JOINUS = PREFIX.concat("info:member:joinus:");
public static final String INFO_MEMBER_SIMPLE = PREFIX.concat("info:member:simple");
public static final String INFO_MEMBER_CATEGORY = PREFIX.concat("info:member:category:");
......
package com.liquidnet.service.adam.dto.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("草莓护照-补签审核搜索参数")
public class AdamCaomeiBadgeApplyAuditSearchParam {
@ApiModelProperty(value = "用户名/姓名")
private String userName;
@ApiModelProperty(value = "审核状态:0-待审核 1-已通过 2-已驳回")
private Integer auditStatus;
}
package com.liquidnet.service.adam.dto.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data
@ApiModel("草莓徽章-补签申请参数")
public class AdamCaomeiBadgeApplyParam {
@NotBlank(message = "徽章ID不能为空")
@ApiModelProperty(value = "徽章ID", required = true)
private String badgeId;
@NotBlank(message = "凭证图片不能为空")
@ApiModelProperty(value = "订单截图/凭证图片URL", required = true)
private String proofImageUrl;
}
package com.liquidnet.service.adam.dto.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import java.util.List;
@Data
@ApiModel("草莓徽章-认领参数")
public class AdamCaomeiBadgeClaimParam {
@NotEmpty(message = "徽章ID列表不能为空")
@ApiModelProperty(value = "徽章ID列表", required = true)
private List<@NotBlank(message = "徽章ID不能为空") String> badgeIds;
}
package com.liquidnet.service.adam.dto.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("草莓护照-徽章领取用户搜索参数")
public class AdamCaomeiBadgeClaimUserSearchParam {
@ApiModelProperty(value = "徽章ID")
private String badgeId;
}
package com.liquidnet.service.adam.dto.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 草莓护照-徽章管理参数
*/
@Data
@ApiModel("草莓护照-徽章管理参数")
public class AdamCaomeiBadgeParam {
@ApiModelProperty(value = "主键ID")
private Long mid;
@ApiModelProperty(value = "徽章ID")
private String badgeId;
@ApiModelProperty(value = "徽章名称")
private String name;
@ApiModelProperty(value = "徽章副标题")
private String subTitle;
@ApiModelProperty(value = "徽章图标 (Emoji字符或图片URL)")
private String icon;
@ApiModelProperty(value = "徽章类型: 1-护照纪念徽章, 2-演出纪念徽章, 3-特殊徽章")
private Integer type;
@ApiModelProperty(value = "关联演出ID (仅演出纪念徽章必填,其他类型为空)")
private String performanceId;
@ApiModelProperty(value = "上架状态: 0-下架(默认), 1-已发布")
private Integer displayStatus;
@ApiModelProperty(value = "排序值,数值越大越靠前")
private Integer sort;
@ApiModelProperty(value = "徽章分享文案")
private String shareText;
}
package com.liquidnet.service.adam.dto.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 草莓护照-徽章搜索参数
*/
@Data
@ApiModel("草莓护照-徽章搜索参数")
public class AdamCaomeiBadgeSearchParam {
@ApiModelProperty(value = "徽章名称")
private String name;
@ApiModelProperty(value = "徽章类型: 1-护照纪念徽章, 2-演出纪念徽章, 3-特殊徽章")
private Integer type;
@ApiModelProperty(value = "上架状态: 0-下架(默认), 1-已发布")
private Integer displayStatus;
@ApiModelProperty(value = "添加时间起(yyyy-MM-dd)")
private String createdAtBegin;
@ApiModelProperty(value = "添加时间止(yyyy-MM-dd)")
private String createdAtEnd;
@ApiModelProperty(value = "当前页码")
private Integer pageNum = 1;
@ApiModelProperty(value = "每页数量")
private Integer pageSize = 10;
}
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.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("草莓护照-护照管理搜索参数")
public class AdamCaomeiPassportSearchParam {
@ApiModelProperty(value = "护照编号")
private String passportNo;
@ApiModelProperty(value = "关联用户名(昵称/真实姓名模糊)")
private String userName;
@ApiModelProperty(value = "手机号")
private String mobile;
@ApiModelProperty(value = "证件号")
private String idCard;
@ApiModelProperty(value = "绑定状态:0-未绑定 1-已绑定 2-已作废,空为全部")
private Integer bindStatus;
}
package com.liquidnet.service.adam.dto.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("二要素认证参数")
public class AdamIdentityParam {
@ApiModelProperty(value = "姓名(手动填写时传)")
private String name;
@ApiModelProperty(value = "证件号(手动填写时传)")
private String idCard;
@ApiModelProperty(value = "入场人ID(选择观演人时传)")
private String entersId;
}
package com.liquidnet.service.adam.dto.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
import java.util.List;
@Data
@ApiModel("草莓护照-补签审核详情")
public class AdamCaomeiBadgeApplyAuditDetailVo {
@ApiModelProperty(value = "申请ID")
private String applyRecordId;
@ApiModelProperty(value = "申请账号")
private String userId;
@ApiModelProperty(value = "用户名")
private String nickname;
@ApiModelProperty(value = "姓名")
private String realName;
@ApiModelProperty(value = "证件号(脱敏)")
private String idCard;
@ApiModelProperty(value = "申请徽章ID")
private String badgeId;
@ApiModelProperty(value = "申请徽章")
private String badgeName;
@ApiModelProperty(value = "关联演出ID")
private String performanceId;
@ApiModelProperty(value = "关联演出")
private String performanceName;
@ApiModelProperty(value = "申请附件")
private String proofImageUrl;
@ApiModelProperty(value = "申请附件 URL 列表(解析自原始字段)")
private List<String> proofImageUrls;
@ApiModelProperty(value = "审核状态:0-待审核 1-已通过 2-已驳回")
private Integer auditStatus;
@ApiModelProperty(value = "驳回理由")
private String rejectReason;
@ApiModelProperty(value = "申请时间")
private Date createdAt;
@ApiModelProperty(value = "审核时间")
private Date updatedAt;
}
package com.liquidnet.service.adam.dto.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
import java.util.List;
@Data
@ApiModel("草莓护照-补签审核列表")
public class AdamCaomeiBadgeApplyAuditVo {
@ApiModelProperty(value = "申请ID")
private String applyRecordId;
@ApiModelProperty(value = "申请账号")
private String userId;
@ApiModelProperty(value = "用户名")
private String nickname;
@ApiModelProperty(value = "姓名")
private String realName;
@ApiModelProperty(value = "证件号(脱敏)")
private String idCard;
@ApiModelProperty(value = "申请徽章")
private String badgeName;
@ApiModelProperty(value = "关联演出")
private String performanceName;
@ApiModelProperty(value = "申请附件")
private String proofImageUrl;
@ApiModelProperty(value = "申请附件 URL 列表(解析自原始字段)")
private List<String> proofImageUrls;
@ApiModelProperty(value = "审核状态:0-待审核 1-已通过 2-已驳回")
private Integer auditStatus;
@ApiModelProperty(value = "申请时间")
private Date createdAt;
}
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 AdamCaomeiBadgeApplyRecordUserVo {
@ApiModelProperty("补签申请ID")
private String applyRecordId;
@ApiModelProperty("申请徽章名称")
private String badgeName;
@ApiModelProperty("申请时间")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DateUtil.DATE_FULL_STR)
private Date applyTime;
@ApiModelProperty("审核状态:0-待审核 1-已通过 2-已驳回")
private Integer auditStatus;
@ApiModelProperty("驳回理由")
private String rejectReason;
@ApiModelProperty("是否可重新上传(仅驳回状态为 true)")
private Boolean canReupload;
@ApiModelProperty("凭证图片")
private String proofImageUrl;
}
package com.liquidnet.service.adam.dto.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
@Data
@ApiModel("草莓护照-徽章领取用户")
public class AdamCaomeiBadgeClaimUserVo {
@ApiModelProperty(value = "用户昵称")
private String nickname;
@ApiModelProperty(value = "手机号(脱敏)")
private String mobile;
@ApiModelProperty(value = "领取时间")
private Date claimedAt;
@ApiModelProperty(value = "获取途径: 1-绑定护照自动发放, 2-购票自动发放, 3-补签审核通过, 4-现场管理员手动发放")
private Integer source;
}
package com.liquidnet.service.adam.dto.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
/**
* 草莓护照-徽章视图对象
*/
@Data
@ApiModel("草莓护照-徽章视图对象")
public class AdamCaomeiBadgeVo {
@ApiModelProperty(value = "主键ID")
private Long mid;
@ApiModelProperty(value = "徽章ID")
private String badgeId;
@ApiModelProperty(value = "徽章名称")
private String name;
@ApiModelProperty(value = "徽章副标题")
private String subTitle;
@ApiModelProperty(value = "徽章图标 (Emoji字符或图片URL)")
private String icon;
@ApiModelProperty(value = "徽章类型: 1-护照纪念徽章, 2-演出纪念徽章, 3-特殊徽章")
private Integer type;
@ApiModelProperty(value = "关联演出ID")
private String performanceId;
@ApiModelProperty(value = "上架状态: 0-下架(默认), 1-已发布")
private Integer displayStatus;
@ApiModelProperty(value = "排序值,数值越大越靠前")
private Integer sort;
@ApiModelProperty(value = "徽章分享文案")
private String shareText;
@ApiModelProperty(value = "添加时间")
private Date createdAt;
@ApiModelProperty(value = "更新时间")
private Date updatedAt;
@ApiModelProperty(value = "领取人数")
private Integer claimedCount;
}
package com.liquidnet.service.adam.dto.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
@Data
@ApiModel("草莓护照-用户已领徽章项")
public class AdamCaomeiPassportBadgeItemVo {
@ApiModelProperty(value = "徽章ID")
private String badgeId;
@ApiModelProperty(value = "徽章名称")
private String badgeName;
@ApiModelProperty(value = "图标")
private String icon;
@ApiModelProperty(value = "领取时间")
private Date claimedAt;
@ApiModelProperty(value = "获取途径 source")
private Integer source;
}
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 subTitle;
@ApiModelProperty("图标")
private String icon;
@ApiModelProperty("分享文案")
private String shareText;
@ApiModelProperty("类型 1护照 2演出 3特殊")
private Integer type;
@ApiModelProperty("关联演出ID(演出纪念徽章)")
private String performanceId;
@ApiModelProperty("关联演出名称(type=2 时用于按演出分组展示;无数据时可能为演出ID)")
private String performanceName;
@ApiModelProperty("是否已认领")
private boolean claimed;
@ApiModelProperty("认领时间(未认领为空)")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DateUtil.DATE_FULL_STR)
private Date claimedAt;
@ApiModelProperty("是否可认领(护照徽章未发放完全,或演出徽章有票未领)")
private boolean claimable;
@ApiModelProperty("是否存在补签待审核(仅演出徽章生效)")
private boolean applyPending;
}
package com.liquidnet.service.adam.dto.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
import java.util.List;
@Data
@ApiModel("草莓护照-护照详情")
public class AdamCaomeiPassportDetailVo {
@ApiModelProperty(value = "护照主键")
private Long mid;
@ApiModelProperty(value = "护照编号")
private String passportNo;
@ApiModelProperty(value = "绑定状态")
private Integer status;
@ApiModelProperty(value = "绑定用户uid")
private String userId;
@ApiModelProperty(value = "首次绑定时间")
private Date boundAt;
@ApiModelProperty(value = "批次号")
private String batchNo;
@ApiModelProperty(value = "用户昵称")
private String nickname;
@ApiModelProperty(value = "手机号(脱敏)")
private String mobile;
@ApiModelProperty(value = "真实姓名")
private String realName;
@ApiModelProperty(value = "证件号(脱敏)")
private String idCard;
@ApiModelProperty(value = "已领取徽章")
private List<AdamCaomeiPassportBadgeItemVo> badges;
}
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("全部上架徽章(扁平列表;演出类含 performanceName,前端可按类型或按演出分组展示)")
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 AdamCaomeiPassportListVo {
@ApiModelProperty(value = "主键")
private Long mid;
@ApiModelProperty(value = "护照ID/编号")
private String passportNo;
@ApiModelProperty(value = "关联用户昵称")
private String nickname;
@ApiModelProperty(value = "真实姓名")
private String realName;
@ApiModelProperty(value = "手机号(脱敏)")
private String mobile;
@ApiModelProperty(value = "证件号(脱敏)")
private String idCard;
@ApiModelProperty(value = "绑定状态 0未绑定 1已绑定 2已作废")
private Integer bindStatus;
@ApiModelProperty(value = "已领取徽章数")
private Integer claimedBadgeCount;
}
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 AdamCaomeiPassportUserCardVo {
@ApiModelProperty("头像")
private String avatar;
@ApiModelProperty("昵称")
private String nickname;
@ApiModelProperty("是否已实名认证")
private boolean realNameVerified;
@ApiModelProperty("护照编号(未绑定时为空)")
private String passportNo;
@ApiModelProperty("护照获得时间")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DateUtil.DATE_FULL_STR)
private Date passportClaimedAt;
@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 subTitle;
@ApiModelProperty("图标")
private String icon;
@ApiModelProperty("分享文案")
private String shareText;
@ApiModelProperty("类型 1护照类型徽章 2演出类型徽章 3特殊徽章")
private Integer type;
@ApiModelProperty("关联演出名称(仅 type=2 有值)")
private String performanceName;
@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.param.AdamCaomeiBadgeApplyParam;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiBadgeApplyRecordUserVo;
import com.liquidnet.service.base.ResponseDto;
import java.util.List;
/**
* 草莓徽章(用户端)
*/
public interface IAdamCaomeiBadgeUserService {
/**
* 认领徽章
*/
ResponseDto<List<String>> claimBadges(List<String> badgeIds, String uid);
/**
* 补签申请记录列表(用户端)
*/
ResponseDto<List<AdamCaomeiBadgeApplyRecordUserVo>> getApplyRecords(String uid);
/**
* 发起补签申请(驳回后可再次提交)
*/
ResponseDto<String> applyBadge(AdamCaomeiBadgeApplyParam param, String uid);
}
package com.liquidnet.service.adam.service;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiPassportHomeVo;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiPassportUserClaimedBadgeVo;
import com.liquidnet.service.base.ResponseDto;
import java.util.List;
/**
* 草莓护照(用户端)
*/
public interface IAdamCaomeiPassportUserService {
/**
* 绑定实体护照:校验编号有效性、是否可绑;通过则写入并发放已上架护照纪念徽章(type=1)
*/
ResponseDto<List<AdamCaomeiPassportUserClaimedBadgeVo>> bindPassport(String passportNo);
/**
* 护照首页:个人信息、实名状态、已认领墙、按类型分组的全部上架徽章
*/
ResponseDto<AdamCaomeiPassportHomeVo> getPassportHome();
/**
* 校验当前登录用户是否已绑定护照
*/
ResponseDto<Boolean> checkPassportBound();
}
......@@ -72,4 +72,13 @@ public interface IAdamUserService {
AdamRealInfoVo identityForUpsert(String uid, String name, String idCard, String mobile, boolean updateFlg);
AdamRealInfoVo identityForUpdate(String uid, String mobile, int idType, int node, String idCard, String idName);
/**
* 二要素认证
* @param uid
* @param name
* @param idCard
* @return
*/
AdamRealInfoVo verifyTwoElements(String uid, String name, String idCard);
}
......@@ -126,6 +126,18 @@ public class BaseController
return rspData;
}
/**
* 响应分页数据(用于查询结果已转为 VO 列表,但需保留 PageHelper 总条数的场景)
*/
protected TableDataInfo getDataTable(PageInfo<?> pageInfo)
{
TableDataInfo rspData = new TableDataInfo();
rspData.setCode(0);
rspData.setRows(pageInfo.getList());
rspData.setTotal(pageInfo.getTotal());
return rspData;
}
/**
* 响应返回结果
*
......
package com.liquidnet.client.admin.web.controller.zhengzai.adam;
import com.github.pagehelper.PageInfo;
import com.liquidnet.client.admin.common.annotation.Log;
import com.liquidnet.client.admin.common.core.controller.BaseController;
import com.liquidnet.client.admin.common.core.domain.AjaxResult;
import com.liquidnet.client.admin.common.core.page.TableDataInfo;
import com.liquidnet.client.admin.common.enums.BusinessType;
import com.liquidnet.client.admin.zhengzai.adam.service.IAdamCaomeiBadgeApplyAuditAdminService;
import com.liquidnet.service.adam.dto.param.AdamCaomeiBadgeApplyAuditSearchParam;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiBadgeApplyAuditDetailVo;
import io.swagger.annotations.Api;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Api(tags = "草莓护照-补签审核")
@Controller
@RequestMapping("adam/caomei/badgeApplyAudit")
public class AdamCaomeiBadgeApplyAuditController extends BaseController {
private final String prefix = "zhengzai/adam/caomei/badgeApplyAudit";
@Autowired
private IAdamCaomeiBadgeApplyAuditAdminService badgeApplyAuditAdminService;
@RequiresPermissions("adam:caomei:badgeApplyAudit:list")
@GetMapping()
public String view() {
return prefix + "/list";
}
@RequiresPermissions("adam:caomei:badgeApplyAudit:list")
@PostMapping("list")
@ResponseBody
public TableDataInfo list(AdamCaomeiBadgeApplyAuditSearchParam param) {
startPage();
PageInfo<?> pageInfo = badgeApplyAuditAdminService.listApplyAudits(param);
return getDataTable(pageInfo);
}
@RequiresPermissions("adam:caomei:badgeApplyAudit:list")
@GetMapping("detail/{applyRecordId}")
public String detail(@PathVariable("applyRecordId") String applyRecordId, ModelMap mmap) {
AdamCaomeiBadgeApplyAuditDetailVo detail = badgeApplyAuditAdminService.getApplyAuditDetail(applyRecordId);
mmap.put("detail", detail);
return prefix + "/detail";
}
@RequiresPermissions("adam:caomei:badgeApplyAudit:edit")
@Log(title = "草莓护照-补签审核:通过", businessType = BusinessType.UPDATE)
@PostMapping("pass")
@ResponseBody
public AjaxResult pass(@RequestParam("applyRecordId") String applyRecordId) {
boolean ok = badgeApplyAuditAdminService.passApplyAudit(applyRecordId);
if (!ok) {
return error("审核通过失败:申请不存在或当前状态不可操作");
}
return success();
}
@RequiresPermissions("adam:caomei:badgeApplyAudit:edit")
@Log(title = "草莓护照-补签审核:驳回", businessType = BusinessType.UPDATE)
@PostMapping("reject")
@ResponseBody
public AjaxResult reject(@RequestParam("applyRecordId") String applyRecordId,
@RequestParam("rejectReason") String rejectReason) {
if (StringUtils.isBlank(rejectReason)) {
return error("驳回理由不能为空");
}
boolean ok = badgeApplyAuditAdminService.rejectApplyAudit(applyRecordId, rejectReason);
if (!ok) {
return error("驳回失败:申请不存在或当前状态不可操作");
}
return success();
}
}
package com.liquidnet.client.admin.web.controller.zhengzai.adam;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.liquidnet.client.admin.common.annotation.Log;
import com.liquidnet.client.admin.common.core.controller.BaseController;
import com.liquidnet.client.admin.common.core.domain.AjaxResult;
import com.liquidnet.client.admin.common.core.page.TableDataInfo;
import com.liquidnet.client.admin.common.enums.BusinessType;
import com.liquidnet.client.admin.zhengzai.adam.service.IAdamCaomeiBadgeAdminService;
import com.liquidnet.commons.lang.util.IDGenerator;
import com.liquidnet.service.adam.dto.param.AdamCaomeiBadgeParam;
import com.liquidnet.service.adam.dto.param.AdamCaomeiBadgeClaimUserSearchParam;
import com.liquidnet.service.adam.dto.param.AdamCaomeiBadgeSearchParam;
import com.github.pagehelper.PageInfo;
import com.liquidnet.service.adam.entity.AdamCaomeiBadge;
import io.swagger.annotations.Api;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.Objects;
@Api(tags = "草莓护照-徽章管理")
@Controller
@RequestMapping("adam/caomei/badge")
public class AdamCaomeiBadgeController extends BaseController {
private final String prefix = "zhengzai/adam/caomei/badge";
private static final int BADGE_NAME_MAX_CHARS = 50;
private static final int BADGE_SUBTITLE_MAX_CHARS = 20;
@Autowired
private IAdamCaomeiBadgeAdminService adamCaomeiBadgeAdminService;
@Value("${liquidnet.client.admin.platformUrl}")
private String platformUrl;
@RequiresPermissions("adam:caomei:badge:list")
@GetMapping()
public String view() {
return prefix + "/badge_list";
}
@RequiresPermissions("adam:caomei:badge:list")
@PostMapping("list")
@ResponseBody
public TableDataInfo list(AdamCaomeiBadgeSearchParam param) {
startPage();
PageInfo<?> pageInfo = adamCaomeiBadgeAdminService.listWithClaimedCount(param);
return getDataTable(pageInfo);
}
@RequiresPermissions("adam:caomei:badge:add")
@GetMapping("add")
public String add(ModelMap mmap) {
mmap.put("platformUrl", platformUrl);
return prefix + "/badge_add";
}
@RequiresPermissions("adam:caomei:badge:add")
@Log(title = "草莓护照-徽章管理:新增", businessType = BusinessType.INSERT)
@PostMapping("add")
@ResponseBody
public AjaxResult addSave(AdamCaomeiBadgeParam param) {
if (param == null) {
return error("参数错误");
}
AjaxResult nameCheck = validateBadgeName(param.getName());
if (nameCheck != null) {
return nameCheck;
}
AjaxResult subTitleCheck = validateBadgeSubTitle(param.getSubTitle());
if (subTitleCheck != null) {
return subTitleCheck;
}
String trimmedName = StringUtils.trimToEmpty(param.getName());
AdamCaomeiBadge badge = new AdamCaomeiBadge();
BeanUtils.copyProperties(param, badge);
badge.setName(trimmedName);
badge.setSubTitle(StringUtils.trimToEmpty(param.getSubTitle()));
badge.setShareText(StringUtils.defaultString(badge.getShareText()));
badge.setBadgeId(IDGenerator.nextSnowId());
badge.setDisplayStatus(0); // 默认下架
if (badge.getSort() == null) {
badge.setSort(0);
}
badge.setCreatedAt(new Date());
badge.setUpdatedAt(new Date());
// 演出类型校验
if (badge.getType() != null && badge.getType() == 2) {
String pid = StringUtils.trimToEmpty(badge.getPerformanceId());
if (StringUtils.isBlank(pid)) {
return error("演出纪念徽章必须关联演出");
}
badge.setPerformanceId(pid);
if (!adamCaomeiBadgeAdminService.kylinPerformanceExists(pid)) {
return error("无相关演出");
}
} else {
badge.setPerformanceId("");
}
return toAjax(adamCaomeiBadgeAdminService.save(badge));
}
@RequiresPermissions("adam:caomei:badge:list")
@GetMapping("detail/{badgeId}")
public String detail(@PathVariable("badgeId") String badgeId, ModelMap mmap) {
AdamCaomeiBadge badge = adamCaomeiBadgeAdminService.getOne(
Wrappers.lambdaQuery(AdamCaomeiBadge.class).eq(AdamCaomeiBadge::getBadgeId, badgeId), false
);
mmap.put("badge", badge);
return prefix + "/badge_detail";
}
@RequiresPermissions("adam:caomei:badge:list")
@GetMapping("claimUsers/{badgeId}")
public String claimUsers(@PathVariable("badgeId") String badgeId, ModelMap mmap) {
mmap.put("badgeId", badgeId);
return prefix + "/badge_claim_users";
}
@RequiresPermissions("adam:caomei:badge:list")
@PostMapping("claimUsers/list")
@ResponseBody
public TableDataInfo claimUsersList(AdamCaomeiBadgeClaimUserSearchParam param) {
startPage();
PageInfo<?> pageInfo = adamCaomeiBadgeAdminService.listClaimUsers(param);
return getDataTable(pageInfo);
}
@RequiresPermissions("adam:caomei:badge:edit")
@GetMapping("edit/{badgeId}")
public String edit(@PathVariable("badgeId") String badgeId, ModelMap mmap) {
AdamCaomeiBadge badge = adamCaomeiBadgeAdminService.getOne(
Wrappers.lambdaQuery(AdamCaomeiBadge.class).eq(AdamCaomeiBadge::getBadgeId, badgeId), false
);
mmap.put("badge", badge);
mmap.put("platformUrl", platformUrl);
return prefix + "/badge_edit";
}
@RequiresPermissions("adam:caomei:badge:edit")
@Log(title = "草莓护照-徽章管理:修改", businessType = BusinessType.UPDATE)
@PostMapping("edit")
@ResponseBody
public AjaxResult editSave(AdamCaomeiBadgeParam param) {
if (param == null || StringUtils.isBlank(param.getBadgeId())) {
return error("参数错误");
}
AdamCaomeiBadge oldBadge = adamCaomeiBadgeAdminService.getOne(
Wrappers.lambdaQuery(AdamCaomeiBadge.class).eq(AdamCaomeiBadge::getBadgeId, param.getBadgeId()), false
);
if (oldBadge == null || oldBadge.getDisplayStatus() == 1) {
return error("已发布的徽章不允许修改");
}
AjaxResult nameCheck = validateBadgeName(param.getName());
if (nameCheck != null) {
return nameCheck;
}
AjaxResult subTitleCheck = validateBadgeSubTitle(param.getSubTitle());
if (subTitleCheck != null) {
return subTitleCheck;
}
String trimmedName = StringUtils.trimToEmpty(param.getName());
if (!Objects.equals(param.getType(), oldBadge.getType())) {
return error("徽章类型不允许修改");
}
AdamCaomeiBadge badge = new AdamCaomeiBadge();
BeanUtils.copyProperties(param, badge);
badge.setName(trimmedName);
badge.setSubTitle(StringUtils.trimToEmpty(param.getSubTitle()));
badge.setShareText(StringUtils.defaultString(badge.getShareText()));
badge.setMid(oldBadge.getMid());
badge.setUpdatedAt(new java.util.Date());
// 徽章类型与「已发布不可改」一致:编辑时不允许变更类型
badge.setType(oldBadge.getType());
if (badge.getType() != null && badge.getType() == 2) {
String pid = StringUtils.trimToEmpty(badge.getPerformanceId());
if (StringUtils.isBlank(pid)) {
return error("演出纪念徽章必须关联演出");
}
badge.setPerformanceId(pid);
if (!adamCaomeiBadgeAdminService.kylinPerformanceExists(pid)) {
return error("无相关演出");
}
} else {
badge.setPerformanceId("");
}
return toAjax(adamCaomeiBadgeAdminService.updateById(badge));
}
@RequiresPermissions("adam:caomei:badge:edit")
@Log(title = "草莓护照-徽章管理:状态修改", businessType = BusinessType.UPDATE)
@PostMapping("changeStatus")
@ResponseBody
public AjaxResult changeStatus(AdamCaomeiBadgeParam param) {
AdamCaomeiBadge oldBadge = adamCaomeiBadgeAdminService.getOne(
Wrappers.lambdaQuery(AdamCaomeiBadge.class).eq(AdamCaomeiBadge::getBadgeId, param.getBadgeId()), false
);
if (oldBadge == null) {
return error("操作失败:未找到徽章");
}
AdamCaomeiBadge updateBadge = new AdamCaomeiBadge();
updateBadge.setMid(oldBadge.getMid());
updateBadge.setDisplayStatus(param.getDisplayStatus());
updateBadge.setUpdatedAt(new java.util.Date());
return toAjax(adamCaomeiBadgeAdminService.updateById(updateBadge));
}
/**
* @return 校验通过返回 null,否则返回错误 AjaxResult
*/
private AjaxResult validateBadgeName(String name) {
String n = StringUtils.trimToEmpty(name);
if (StringUtils.isBlank(n)) {
return error("徽章名称不能为空");
}
int len = n.codePointCount(0, n.length());
if (len > BADGE_NAME_MAX_CHARS) {
return error("徽章名称不能超过" + BADGE_NAME_MAX_CHARS + "个字");
}
return null;
}
private AjaxResult validateBadgeSubTitle(String subTitle) {
String s = StringUtils.trimToEmpty(subTitle);
if (StringUtils.isBlank(s)) {
return null;
}
int len = s.codePointCount(0, s.length());
if (len > BADGE_SUBTITLE_MAX_CHARS) {
return error("徽章副标题不能超过" + BADGE_SUBTITLE_MAX_CHARS + "个字");
}
return null;
}
}
package com.liquidnet.client.admin.web.controller.zhengzai.adam;
import com.liquidnet.client.admin.common.annotation.Log;
import com.liquidnet.client.admin.common.core.controller.BaseController;
import com.liquidnet.client.admin.common.core.domain.AjaxResult;
import com.liquidnet.client.admin.common.core.page.TableDataInfo;
import com.liquidnet.client.admin.common.enums.BusinessType;
import com.liquidnet.client.admin.zhengzai.adam.service.IAdamCaomeiPassportAdminService;
import com.liquidnet.service.adam.dto.param.AdamCaomeiPassportSearchParam;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiPassportDetailVo;
import com.github.pagehelper.PageInfo;
import io.swagger.annotations.Api;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*;
@Api(tags = "草莓护照-护照管理")
@Controller
@RequestMapping("adam/caomei/passport")
public class AdamCaomeiPassportController extends BaseController {
private final String prefix = "zhengzai/adam/caomei/passport";
@Autowired
private IAdamCaomeiPassportAdminService adamCaomeiPassportAdminService;
@RequiresPermissions("adam:caomei:passport:list")
@GetMapping()
public String view() {
return prefix + "/passport_list";
}
@RequiresPermissions("adam:caomei:passport:list")
@PostMapping("list")
@ResponseBody
public TableDataInfo list(AdamCaomeiPassportSearchParam param) {
startPage();
PageInfo<?> pageInfo = adamCaomeiPassportAdminService.listPassports(param);
return getDataTable(pageInfo);
}
@RequiresPermissions("adam:caomei:passport:list")
@GetMapping("bound/count")
@ResponseBody
public AjaxResult boundCount() {
return AjaxResult.success(adamCaomeiPassportAdminService.countBoundPassports());
}
@RequiresPermissions("adam:caomei:passport:list")
@GetMapping("detail/{passportNo}")
public String detail(@PathVariable("passportNo") String passportNo, ModelMap mmap) {
AdamCaomeiPassportDetailVo detail = adamCaomeiPassportAdminService.getPassportDetail(passportNo);
mmap.put("detail", detail);
return prefix + "/passport_detail";
}
@RequiresPermissions("adam:caomei:passport:edit")
@Log(title = "草莓护照-护照解绑", businessType = BusinessType.UPDATE)
@PostMapping("unbind")
@ResponseBody
public AjaxResult unbind(@RequestParam("passportNo") String passportNo) {
boolean ok = adamCaomeiPassportAdminService.unbindPassport(passportNo);
if (!ok) {
return error("解绑失败:护照不存在或当前未绑定用户");
}
return success();
}
}
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" >
<head>
<th:block th:include="include :: header('新增徽章')" />
<th:block th:include="include :: bootstrap-fileinput-css" />
</head>
<body class="white-bg">
<div class="wrapper wrapper-content animated fadeInRight ibox-content">
<form class="form-horizontal m" id="form-badge-add">
<div class="form-group">
<label class="col-sm-3 control-label is-required">徽章名称:</label>
<div class="col-sm-8">
<input name="name" class="form-control" type="text" maxlength="50" required placeholder="最多50个字">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">徽章副标题:</label>
<div class="col-sm-8">
<input name="subTitle" class="form-control" type="text" maxlength="20" placeholder="最多20个字">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label is-required">徽章图标:</label>
<div class="col-sm-8">
<input type="hidden" name="icon" id="badgeIconUrl" value="">
<div id="badgeIconPreviewWrap" class="m-b" style="display:none;">
<img id="badgeIconPreview" src="" alt="预览" style="max-height:96px;border-radius:4px;border:1px solid #eee;"/>
</div>
<div class="file-loading">
<input id="fileinput-badge-icon" type="file" name="file" data-browse-on-zone-click="true">
</div>
<span class="help-block m-b-none"><i class="fa fa-info-circle"></i> 支持 jpg、png、gif、webp,单张最大 5M;上传后自动保存为图片地址。</span>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label is-required">徽章类型:</label>
<div class="col-sm-8">
<select name="type" class="form-control m-b" required onchange="typeChange(this.value)">
<option value="">请选择</option>
<option value="1">护照纪念徽章</option>
<option value="2">演出纪念徽章</option>
<!-- <option value="3">特殊徽章</option> -->
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">排序:</label>
<div class="col-sm-8">
<input name="sort" class="form-control" type="number" min="0" value="0" placeholder="数值越大越靠前">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">分享文案:</label>
<div class="col-sm-8">
<textarea name="shareText" class="form-control" rows="3" maxlength="255" placeholder="请输入徽章分享文案"></textarea>
</div>
</div>
<div class="form-group" id="ticketTimesDiv" style="display: none;">
<label class="col-sm-3 control-label is-required">关联演出:</label>
<div class="col-sm-8">
<input name="performanceId" id="performanceId" class="form-control" type="text" placeholder="请输入演出ID">
<span class="help-block m-b-none"><i class="fa fa-info-circle"></i> 演出纪念徽章必填</span>
</div>
</div>
</form>
</div>
<th:block th:include="include :: footer" />
<th:block th:include="include :: bootstrap-fileinput-js" />
<script type="text/javascript" th:inline="javascript">
var prefix = ctx + "adam/caomei/badge";
var platformUrl = /*[[${platformUrl}]]*/ '';
function typeChange(val) {
if (val == 2) {
$("#ticketTimesDiv").show();
$("#performanceId").prop("required", true);
} else {
$("#ticketTimesDiv").hide();
$("#performanceId").prop("required", false);
$("#performanceId").val("");
}
}
$("#form-badge-add").validate({
focusCleanup: true
});
function submitHandler() {
if (!$("#badgeIconUrl").val()) {
$.modal.msgWarning("请先上传徽章图标");
return;
}
if ($.validate.form()) {
$.operate.save(prefix + "/add", $('#form-badge-add').serialize());
}
}
$(function () {
$("#fileinput-badge-icon").fileinput({
'theme': 'explorer-fas',
'uploadUrl': platformUrl + "/platform/basicServices/alOss/upload/unsm",
"uploadExtraData": {
"pathName": "other",
"buckType": 1
},
autoReplace: true,
dropZoneTitle: "点击或拖拽上传徽章图标",
maxFileCount: 1,
maxFileSize: 5120,
allowedFileExtensions: ['jpg', 'jpeg', 'png', 'gif', 'webp'],
msgSizeTooLarge: '文件 "{name}" ({size} KB) 超过了允许大小 {maxSize} KB,最大支持上传5M文件'
}).on("filebatchselected", function () {
$(this).fileinput("upload");
}).on("fileuploaded", function (event, data) {
if (!data.response || !data.response.data || !data.response.data.ossPath) {
$.modal.alertWarning((data.response && data.response.msg) ? data.response.msg : "图片上传失败");
return;
}
var fullUrl = "https://img.zhengzai.tv/" + data.response.data.ossPath;
$("#badgeIconUrl").val(fullUrl);
$("#badgeIconPreview").attr("src", fullUrl);
$("#badgeIconPreviewWrap").show();
});
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:include="include :: header('领取用户列表')" />
</head>
<body class="gray-bg">
<div class="container-div">
<div class="row">
<input type="hidden" id="badgeId" th:value="${badgeId}">
<div class="col-sm-12 select-table table-striped">
<table id="bootstrap-table"></table>
</div>
</div>
</div>
<th:block th:include="include :: footer" />
<script th:inline="javascript">
var prefix = ctx + "adam/caomei/badge";
$(function() {
var options = {
url: prefix + "/claimUsers/list",
queryParams: function(params) {
var curParams = {
pageSize: params.limit,
pageNum: params.offset / params.limit + 1,
searchValue: params.search,
orderByColumn: params.sort,
isAsc: params.order,
badgeId: $("#badgeId").val()
};
return curParams;
},
modalName: "领取用户",
columns: [
{
field: 'nickname',
title: '用户昵称'
},
{
field: 'mobile',
title: '手机号'
},
{
field: 'claimedAt',
title: '领取时间'
},
{
field: 'source',
title: '获取途径',
formatter: function(value, row, index) {
if (value == 1) return '绑定护照自动发放';
if (value == 2) return '购票自动发放';
if (value == 3) return '补签审核通过';
if (value == 4) return '现场管理员手动发放';
return '未知';
}
}
]
};
$.table.init(options);
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" >
<head>
<th:block th:include="include :: header('徽章详情')" />
</head>
<body class="white-bg">
<div class="wrapper wrapper-content animated fadeInRight ibox-content">
<form class="form-horizontal m" id="form-badge-detail" th:object="${badge}">
<div class="form-group">
<label class="col-sm-3 control-label">徽章名称:</label>
<div class="col-sm-8">
<div class="form-control-static" th:text="*{name}"></div>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">徽章副标题:</label>
<div class="col-sm-8">
<div class="form-control-static" th:text="*{subTitle != null and subTitle != '' ? subTitle : '-'}"></div>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">徽章图标:</label>
<div class="col-sm-8">
<div class="form-control-static">
<span th:if="*{icon != null and (!icon.startsWith('http') and !icon.startsWith('/'))}" style="font-size: 24px;" th:text="*{icon}"></span>
<img th:if="*{icon != null and (icon.startsWith('http') or icon.startsWith('/'))}" th:src="*{icon}" style="max-height: 30px;"/>
</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">徽章类型:</label>
<div class="col-sm-8">
<div class="form-control-static" th:if="*{type == 1}">护照纪念徽章</div>
<div class="form-control-static" th:if="*{type == 2}">演出纪念徽章</div>
<div class="form-control-static" th:if="*{type == 3}">特殊徽章</div>
</div>
</div>
<div class="form-group" th:style="${badge.type == 2 ? 'display:block;' : 'display:none;'}">
<label class="col-sm-3 control-label">关联演出:</label>
<div class="col-sm-8">
<div class="form-control-static" th:text="*{performanceId}"></div>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">状态:</label>
<div class="col-sm-8">
<div class="form-control-static" th:if="*{displayStatus == 1}">已发布</div>
<div class="form-control-static" th:if="*{displayStatus == 0}">下架</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">排序:</label>
<div class="col-sm-8">
<div class="form-control-static" th:text="*{sort}"></div>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">分享文案:</label>
<div class="col-sm-8">
<div class="form-control-static" th:text="*{shareText}"></div>
</div>
</div>
</form>
</div>
<th:block th:include="include :: footer" />
<script type="text/javascript">
$(function() {
// 隐藏底部的确认按钮,只保留关闭按钮
var index = parent.layer.getFrameIndex(window.name);
if (index) {
var $layero = parent.$("#layui-layer" + index);
$layero.find(".layui-layer-btn0").hide(); // 隐藏确认按钮
}
});
</script>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" >
<head>
<th:block th:include="include :: header('修改徽章')" />
<th:block th:include="include :: bootstrap-fileinput-css" />
</head>
<body class="white-bg">
<div class="wrapper wrapper-content animated fadeInRight ibox-content">
<form class="form-horizontal m" id="form-badge-edit" th:object="${badge}">
<input name="badgeId" th:field="*{badgeId}" type="hidden">
<div class="form-group">
<label class="col-sm-3 control-label is-required">徽章名称:</label>
<div class="col-sm-8">
<input name="name" th:field="*{name}" class="form-control" type="text" maxlength="50" required placeholder="最多50个字">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">徽章副标题:</label>
<div class="col-sm-8">
<input name="subTitle" th:field="*{subTitle}" class="form-control" type="text" maxlength="20" placeholder="最多20个字">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label is-required">徽章图标:</label>
<div class="col-sm-8">
<input type="hidden" name="icon" id="badgeIconUrl" th:value="*{icon}">
<div id="badgeIconPreviewWrap" class="m-b"
th:style="${badge.icon != null and (#strings.startsWith(badge.icon, 'http') or #strings.startsWith(badge.icon, '/'))} ? '' : 'display:none;'">
<img id="badgeIconPreview" alt="当前图标"
th:src="${badge.icon != null and (#strings.startsWith(badge.icon, 'http') or #strings.startsWith(badge.icon, '/'))} ? ${badge.icon} : ''"
style="max-height:96px;border-radius:4px;border:1px solid #eee;"/>
</div>
<p id="badgeIconLegacyHint" class="help-block"
th:if="${badge.icon != null and !(#strings.startsWith(badge.icon, 'http') or #strings.startsWith(badge.icon, '/'))}">
当前为 Emoji 文本:<strong th:text="${badge.icon}"></strong>,上传图片保存后将替换为图片地址。
</p>
<div class="file-loading">
<input id="fileinput-badge-icon" type="file" name="file" data-browse-on-zone-click="true">
</div>
<span class="help-block m-b-none"><i class="fa fa-info-circle"></i> 支持 jpg、png、gif、webp,单张最大 5M;重新上传将覆盖地址。</span>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label is-required">徽章类型:</label>
<div class="col-sm-8">
<input type="hidden" name="type" th:value="*{type}"/>
<select class="form-control m-b" disabled>
<option th:selected="${badge.type == 1}" value="1">护照纪念徽章</option>
<option th:selected="${badge.type == 2}" value="2">演出纪念徽章</option>
</select>
<span class="help-block m-b-none"><i class="fa fa-info-circle"></i> 徽章类型保存后不可修改</span>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">排序:</label>
<div class="col-sm-8">
<input name="sort" th:field="*{sort}" class="form-control" type="number" min="0" placeholder="数值越大越靠前">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">分享文案:</label>
<div class="col-sm-8">
<textarea name="shareText" th:field="*{shareText}" class="form-control" rows="3" maxlength="255" placeholder="请输入徽章分享文案"></textarea>
</div>
</div>
<div class="form-group" id="ticketTimesDiv" th:style="${badge.type == 2 ? 'display:block;' : 'display:none;'}">
<label class="col-sm-3 control-label is-required">关联演出:</label>
<div class="col-sm-8">
<input name="performanceId" id="performanceId" th:field="*{performanceId}" class="form-control" type="text" placeholder="请输入演出ID" th:required="${badge.type == 2}">
<span class="help-block m-b-none"><i class="fa fa-info-circle"></i> 演出纪念徽章必填</span>
</div>
</div>
</form>
</div>
<th:block th:include="include :: footer" />
<th:block th:include="include :: bootstrap-fileinput-js" />
<script type="text/javascript" th:inline="javascript">
var prefix = ctx + "adam/caomei/badge";
var platformUrl = /*[[${platformUrl}]]*/ '';
function typeChange(val) {
if (val == 2 || val == '2') {
$("#ticketTimesDiv").show();
$("#performanceId").prop("required", true);
} else {
$("#ticketTimesDiv").hide();
$("#performanceId").prop("required", false);
$("#performanceId").val("");
}
}
$(function () {
typeChange($('input[name="type"]').val());
$("#fileinput-badge-icon").fileinput({
'theme': 'explorer-fas',
'uploadUrl': platformUrl + "/platform/basicServices/alOss/upload/unsm",
"uploadExtraData": {
"pathName": "other",
"buckType": 1
},
autoReplace: true,
dropZoneTitle: "点击或拖拽上传徽章图标(可选,用于更换)",
maxFileCount: 1,
maxFileSize: 5120,
allowedFileExtensions: ['jpg', 'jpeg', 'png', 'gif', 'webp'],
msgSizeTooLarge: '文件 "{name}" ({size} KB) 超过了允许大小 {maxSize} KB,最大支持上传5M文件'
}).on("filebatchselected", function () {
$(this).fileinput("upload");
}).on("fileuploaded", function (event, data) {
if (!data.response || !data.response.data || !data.response.data.ossPath) {
$.modal.alertWarning((data.response && data.response.msg) ? data.response.msg : "图片上传失败");
return;
}
var fullUrl = "https://img.zhengzai.tv/" + data.response.data.ossPath;
$("#badgeIconUrl").val(fullUrl);
$("#badgeIconPreview").attr("src", fullUrl);
$("#badgeIconPreviewWrap").show();
$("#badgeIconLegacyHint").hide();
});
});
$("#form-badge-edit").validate({
focusCleanup: true
});
function submitHandler() {
if (!$("#badgeIconUrl").val()) {
$.modal.msgWarning("请先上传徽章图标(若为历史 Emoji 数据,请上传一张图片作为图标)");
return;
}
if ($.validate.form()) {
$.operate.save(prefix + "/edit", $('#form-badge-edit').serialize());
}
}
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<th:block th:include="include :: header('草莓护照徽章列表')" />
</head>
<body class="gray-bg">
<div class="container-div">
<div class="row">
<div class="col-sm-12 search-collapse">
<form id="formId">
<div class="select-list">
<ul>
<li>
徽章名称:<input type="text" name="name"/>
</li>
<li>
徽章类型:<select name="type" th:with="type=${@dict.getType('adam_caomei_badge_type')}">
<option value="">所有</option>
<option value="1">护照纪念徽章</option>
<option value="2">演出纪念徽章</option>
<!-- <option value="3">特殊徽章</option> -->
</select>
</li>
<li>
状态:<select name="displayStatus">
<option value="">所有</option>
<option value="0">下架</option>
<option value="1">已发布</option>
</select>
</li>
<li class="select-time">
<label>添加时间:</label>
<input type="text" class="time-input" id="badgeCreatedBegin" placeholder="开始日期" name="createdAtBegin"/>
<span> - </span>
<input type="text" class="time-input" id="badgeCreatedEnd" placeholder="结束日期" name="createdAtEnd"/>
</li>
<li>
<a class="btn btn-primary btn-rounded btn-sm" onclick="$.table.search()"><i class="fa fa-search"></i>&nbsp;搜索</a>
<a class="btn btn-warning btn-rounded btn-sm" onclick="$.form.reset()"><i class="fa fa-refresh"></i>&nbsp;重置</a>
</li>
</ul>
</div>
</form>
</div>
<div class="btn-group-sm" id="toolbar" role="group">
<a class="btn btn-success" onclick="$.operate.add()" shiro:hasPermission="adam:caomei:badge:add">
<i class="fa fa-plus"></i> 新增
</a>
</div>
<div class="col-sm-12 select-table table-striped">
<table id="bootstrap-table"></table>
</div>
</div>
</div>
<th:block th:include="include :: footer" />
<script th:inline="javascript">
var editFlag = [[${@permission.hasPermi('adam:caomei:badge:edit')}]];
var prefix = ctx + "adam/caomei/badge";
$(function() {
var options = {
url: prefix + "/list",
createUrl: prefix + "/add",
updateUrl: prefix + "/edit/{id}",
modalName: "草莓护照徽章",
columns: [{
checkbox: true
},
{
field: 'mid',
title: '主键ID',
visible: false
},
{
field: 'badgeId',
title: '徽章ID'
},
{
field: 'icon',
title: '图标',
formatter: function(value, row, index) {
// 如果是图片链接,可以用 $.table.imageView(value)
// 如果是 emoji,直接返回
if(value && (value.startsWith('http') || value.startsWith('/'))) {
return $.table.imageView(value);
}
return '<span style="font-size: 24px;">' + value + '</span>';
}
},
{
field: 'name',
title: '名称'
},
{
field: 'subTitle',
title: '副标题',
formatter: function(value) {
return value ? value : '-';
}
},
{
field: 'type',
title: '类型',
formatter: function(value, row, index) {
if (value == 1) return '<span class="badge badge-info">护照纪念</span>';
if (value == 2) return '<span class="badge badge-primary">演出纪念</span>';
if (value == 3) return '<span class="badge badge-warning">特殊徽章</span>';
return value;
}
},
{
field: 'createdAt',
title: '添加时间',
formatter: function(value, row, index) {
if (!value) return '-';
return value.length >= 10 ? value.substring(0, 10) : value;
}
},
{
field: 'claimedCount',
title: '领取人数'
},
{
field: 'sort',
title: '排序(越大越靠前)'
},
{
field: 'displayStatus',
title: '状态',
align: 'center',
formatter: function (value, row, index) {
if (value == 1) {
return '<span class="badge badge-primary" style="background-color: #e6f3ff; color: #1890ff; padding: 5px 10px; border-radius: 4px;">已发布</span>';
} else {
return '<span class="badge badge-default" style="background-color: #f5f5f5; color: #999; padding: 5px 10px; border-radius: 4px;">下架</span>';
}
}
},
{
title: '操作',
align: 'center',
formatter: function(value, row, index) {
var actions = [];
// 详情按钮
actions.push('<a class="btn btn-info btn-sm" style="margin-right: 5px;" href="javascript:void(0)" onclick="detail(\'' + row.badgeId + '\')"><i class="fa fa-eye"></i>详情</a>');
// 仅下架状态可修改(编辑)
if (row.displayStatus == 0) {
actions.push('<a class="btn btn-success btn-sm ' + editFlag + '" style="margin-right: 5px;" href="javascript:void(0)" onclick="$.operate.edit(\'' + row.badgeId + '\')"><i class="fa fa-edit"></i>编辑</a>');
}
// 下架/发布按钮
if (row.displayStatus == 1) {
actions.push('<a class="btn btn-danger btn-sm" style="margin-right: 5px;" href="javascript:void(0)" onclick="disable(\'' + row.badgeId + '\')"><i class="fa fa-arrow-down"></i>下架</a>');
} else {
actions.push('<a class="btn btn-primary btn-sm" style="margin-right: 5px;" href="javascript:void(0)" onclick="enable(\'' + row.badgeId + '\')"><i class="fa fa-upload"></i>发布</a>');
}
// 领取用户按钮
actions.push('<a class="btn btn-warning btn-sm" href="javascript:void(0)" onclick="claimedUsers(\'' + row.badgeId + '\')"><i class="fa fa-users"></i>领取用户</a>');
return actions.join('');
}
}]
};
$.table.init(options);
});
/* 徽章下架 */
function disable(badgeId) {
$.modal.confirm("确认要下架该徽章吗?", function() {
$.operate.post(prefix + "/changeStatus", { "badgeId": badgeId, "displayStatus": 0 });
});
}
/* 徽章发布 */
function enable(badgeId) {
$.modal.confirm("确认要发布该徽章吗?", function() {
$.operate.post(prefix + "/changeStatus", { "badgeId": badgeId, "displayStatus": 1 });
});
}
/* 详情 */
function detail(badgeId) {
$.modal.open("徽章详情", prefix + "/detail/" + badgeId);
}
/* 领取用户 */
function claimedUsers(badgeId) {
if (!badgeId) {
$.modal.msgError("未找到徽章ID");
return;
}
$.modal.openOptions({
title: "领取用户",
url: prefix + "/claimUsers/" + badgeId,
skin: 'layui-layer-gray',
btn: ['关闭'],
yes: function (index, layero) {
layer.close(index);
}
});
}
</script>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:include="include :: header('补签详情')" />
</head>
<body class="gray-bg">
<div class="container-div" th:if="${detail != null}">
<div class="row">
<div class="col-sm-12">
<div class="ibox">
<div class="ibox-title"><h5>申请信息</h5></div>
<div class="ibox-content">
<table class="table table-bordered table-striped">
<tbody>
<tr><td class="col-sm-2">申请ID</td><td th:text="${detail.applyRecordId}">-</td></tr>
<tr><td>申请账号</td><td th:text="${detail.userId}">-</td></tr>
<tr><td>用户名</td><td th:text="${detail.nickname != null && detail.nickname != '' ? detail.nickname : '-'}">-</td></tr>
<tr><td>姓名</td><td th:text="${detail.realName != null && detail.realName != '' ? detail.realName : '-'}">-</td></tr>
<tr><td>证件号</td><td th:text="${detail.idCard != null && detail.idCard != '' ? detail.idCard : '-'}">-</td></tr>
<tr><td>申请徽章</td><td th:text="${detail.badgeName != null && detail.badgeName != '' ? detail.badgeName : '-'}">-</td></tr>
<tr><td>关联演出</td><td th:text="${detail.performanceName != null && detail.performanceName != '' ? detail.performanceName : '-'}">-</td></tr>
<tr>
<td>申请附件</td>
<td>
<span th:if="${detail.proofImageUrls == null or detail.proofImageUrls.isEmpty()}">-</span>
<a th:each="u : ${detail.proofImageUrls}" th:href="${u}" target="_blank" style="display:inline-block;margin:0 8px 8px 0;">
<img th:src="${u}" style="max-height: 90px; border-radius: 4px;"/>
</a>
</td>
</tr>
<tr>
<td>审核状态</td>
<td>
<span th:if="${detail.auditStatus == 0}" class="label label-warning">待审核</span>
<span th:if="${detail.auditStatus == 1}" class="label label-success">已通过</span>
<span th:if="${detail.auditStatus == 2}" class="label label-danger">已驳回</span>
</td>
</tr>
<tr><td>申请时间</td><td th:text="${detail.createdAt != null ? #dates.format(detail.createdAt, 'yyyy-MM-dd HH:mm:ss') : '-'}">-</td></tr>
<tr><td>审核时间</td><td th:text="${detail.updatedAt != null ? #dates.format(detail.updatedAt, 'yyyy-MM-dd HH:mm:ss') : '-'}">-</td></tr>
<tr th:if="${detail.auditStatus == 2}">
<td>驳回理由</td>
<td th:text="${detail.rejectReason != null && detail.rejectReason != '' ? detail.rejectReason : '-'}">-</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="container-div" th:if="${detail == null}">
<div class="alert alert-warning">未找到申请信息</div>
</div>
<th:block th:include="include :: footer" />
<script type="text/javascript">
$(function() {
// 详情弹窗无需确认按钮,仅保留关闭
var index = parent.layer.getFrameIndex(window.name);
if (index) {
var $layero = parent.$("#layui-layer" + index);
$layero.find(".layui-layer-btn0").hide();
}
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<th:block th:include="include :: header('补签审核')" />
</head>
<body class="gray-bg">
<div class="container-div">
<div class="row">
<div class="col-sm-12 search-collapse">
<form id="formId">
<div class="select-list">
<ul>
<li>
用户名/姓名:<input type="text" name="userName" placeholder="昵称或姓名"/>
</li>
<li>
申请状态:
<select name="auditStatus">
<option value="">全部</option>
<option value="0">待审核</option>
<option value="1">已通过</option>
<option value="2">已驳回</option>
</select>
</li>
<li>
<a class="btn btn-primary btn-rounded btn-sm" onclick="$.table.search()"><i class="fa fa-search"></i>&nbsp;筛选</a>
<a class="btn btn-warning btn-rounded btn-sm" onclick="$.form.reset()"><i class="fa fa-refresh"></i>&nbsp;重置</a>
</li>
</ul>
</div>
</form>
</div>
<div class="col-sm-12 select-table table-striped">
<table id="bootstrap-table"></table>
</div>
</div>
</div>
<th:block th:include="include :: footer" />
<script th:inline="javascript">
var detailFlag = [[${@permission.hasPermi('adam:caomei:badgeApplyAudit:list')}]];
var passFlag = [[${@permission.hasPermi('adam:caomei:badgeApplyAudit:edit')}]];
var rejectFlag = [[${@permission.hasPermi('adam:caomei:badgeApplyAudit:edit')}]];
var prefix = ctx + "adam/caomei/badgeApplyAudit";
$(function() {
var options = {
url: prefix + "/list",
formId: "formId",
modalName: "补签审核",
columns: [
{ field: 'applyRecordId', title: '申请ID' },
{ field: 'userId', title: '申请账号' },
{ field: 'realName', title: '姓名', formatter: function(v) { return v || '-'; } },
{ field: 'idCard', title: '证件号', formatter: function(v) { return v || '-'; } },
{ field: 'badgeName', title: '申请徽章', formatter: function(v) { return v || '-'; } },
{ field: 'performanceName', title: '关联演出', formatter: function(v) { return v || '-'; } },
{
field: 'proofImageUrl',
title: '申请附件',
align: 'center',
formatter: function(value, row) {
var urls = row.proofImageUrls;
if (!urls || urls.length === 0) {
if (!value) return '-';
urls = [value];
}
var html = [];
for (var i = 0; i < urls.length; i++) {
var u = urls[i];
if (!u) continue;
html.push('<a href="' + u + '" target="_blank" title="查看原图"><img src="' + u + '" style="height:32px;max-width:60px;margin:0 2px;cursor:pointer;border-radius:3px;"/></a>');
}
return html.length ? html.join('') : '-';
}
},
{
field: 'auditStatus',
title: '审核状态',
align: 'center',
formatter: function(value) {
if (value === 0) return '<span class="label label-warning">待审核</span>';
if (value === 1) return '<span class="label label-success">已通过</span>';
if (value === 2) return '<span class="label label-danger">已驳回</span>';
return '-';
}
},
{
title: '操作',
align: 'center',
formatter: function(value, row) {
var actions = [];
actions.push('<a class="btn btn-info btn-xs ' + detailFlag + '" href="javascript:void(0)" onclick=\'detail(' + JSON.stringify(row.applyRecordId) + ')\'><i class="fa fa-eye"></i>详情</a> ');
if (row.auditStatus === 0) {
actions.push('<a class="btn btn-success btn-xs ' + passFlag + '" href="javascript:void(0)" onclick=\'passAudit(' + JSON.stringify(row.applyRecordId) + ')\'><i class="fa fa-check"></i>通过</a> ');
actions.push('<a class="btn btn-danger btn-xs ' + rejectFlag + '" href="javascript:void(0)" onclick=\'rejectAudit(' + JSON.stringify(row.applyRecordId) + ')\'><i class="fa fa-close"></i>驳回</a>');
}
return actions.join('');
}
}
]
};
$.table.init(options);
});
function detail(applyRecordId) {
$.modal.open("补签详情", prefix + "/detail/" + encodeURIComponent(applyRecordId));
}
function passAudit(applyRecordId) {
$.modal.confirm("确认审核通过并为用户发放对应徽章吗?", function() {
$.operate.post(prefix + "/pass", { applyRecordId: applyRecordId });
});
}
function rejectAudit(applyRecordId) {
layer.prompt({
title: "请输入驳回理由",
formType: 2,
maxlength: 255
}, function(value, index) {
layer.close(index);
var reason = $.trim(value);
if (!reason) {
$.modal.msgError("驳回理由不能为空");
return;
}
$.operate.post(prefix + "/reject", { applyRecordId: applyRecordId, rejectReason: reason });
});
}
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:include="include :: header('护照详情')" />
</head>
<body class="gray-bg">
<div class="container-div" th:if="${detail != null}">
<div class="row">
<div class="col-sm-12">
<div class="ibox">
<div class="ibox-title"><h5>护照信息</h5></div>
<div class="ibox-content">
<table class="table table-bordered table-striped">
<tbody>
<tr><td class="col-sm-2">护照编号</td><td th:text="${detail.passportNo}">-</td></tr>
<tr><td>绑定状态</td>
<td>
<span th:if="${detail.status == 1}" class="label label-info">已绑定</span>
<span th:if="${detail.status == 0}" class="label label-default">未绑定</span>
<span th:if="${detail.status == 2}" class="label label-warning">已作废</span>
</td>
</tr>
<tr><td>批次号</td><td th:text="${detail.batchNo != null ? detail.batchNo : '-'}">-</td></tr>
<tr><td>首次绑定时间</td><td th:text="${detail.boundAt != null ? #dates.format(detail.boundAt, 'yyyy-MM-dd HH:mm:ss') : '-'}">-</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-sm-12" th:if="${detail.userId != null and detail.userId != ''}">
<div class="ibox">
<div class="ibox-title"><h5>关联用户信息</h5></div>
<div class="ibox-content">
<table class="table table-bordered table-striped">
<tbody>
<tr><td class="col-sm-2">用户ID</td><td th:text="${detail.userId}">-</td></tr>
<tr><td>昵称</td><td th:text="${detail.nickname}">-</td></tr>
<tr><td>手机号</td><td th:text="${detail.mobile}">-</td></tr>
<tr><td>真实姓名</td><td th:text="${detail.realName}">-</td></tr>
<tr><td>证件号</td><td th:text="${detail.idCard}">-</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-sm-12" th:if="${detail.userId == null or detail.userId == ''}">
<div class="alert alert-info">当前护照未绑定用户</div>
</div>
<div class="col-sm-12" th:if="${detail.badges != null and !#lists.isEmpty(detail.badges)}">
<div class="ibox">
<div class="ibox-title"><h5>已领取徽章</h5></div>
<div class="ibox-content">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>徽章名称</th>
<th>图标</th>
<th>领取时间</th>
<th>获取途径</th>
</tr>
</thead>
<tbody>
<tr th:each="b : ${detail.badges}">
<td th:text="${b.badgeName}">-</td>
<td>
<span th:if="${b.icon != null and (b.icon.startsWith('http') or b.icon.startsWith('/'))}">
<img th:src="${b.icon}" style="max-height:28px;"/>
</span>
<span th:if="${b.icon != null and !(b.icon.startsWith('http') or b.icon.startsWith('/'))}" th:text="${b.icon}" style="font-size:22px;"></span>
</td>
<td th:text="${b.claimedAt != null ? #dates.format(b.claimedAt, 'yyyy-MM-dd HH:mm:ss') : '-'}">-</td>
<td>
<span th:if="${b.source == 1}">绑定护照自动发放</span>
<span th:if="${b.source == 2}">购票自动发放</span>
<span th:if="${b.source == 3}">补签审核通过</span>
<span th:if="${b.source == 4}">现场管理员手动发放</span>
<span th:if="${b.source == null or b.source &lt; 1 or b.source &gt; 4}">未知</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="container-div" th:if="${detail == null}">
<div class="alert alert-warning">未找到护照信息</div>
</div>
<th:block th:include="include :: footer" />
</body>
</html>
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<th:block th:include="include :: header('护照管理')" />
</head>
<body class="gray-bg">
<div class="container-div">
<div class="row">
<div class="col-sm-12 search-collapse">
<form id="formId">
<div class="select-list">
<ul>
<li>
<label>护照编号:</label>
<input type="text" name="passportNo" placeholder="模糊匹配"/>
</li>
<li>
<label>绑定状态:</label>
<select name="bindStatus">
<option value="">全部</option>
<option value="1">已绑定</option>
<option value="0">未绑定</option>
<option value="2">已作废</option>
</select>
</li>
<li>
<label>用户昵称/姓名:</label>
<input type="text" name="userName" placeholder="昵称或真实姓名"/>
</li>
<li>
<label>手机号:</label>
<input type="text" name="mobile" placeholder="完整或片段"/>
</li>
<li>
<label>证件号:</label>
<input type="text" name="idCard" placeholder="完整或片段"/>
</li>
<li>
<a class="btn btn-primary btn-rounded btn-sm" onclick="$.table.search()"><i class="fa fa-search"></i>&nbsp;搜索</a>
<a class="btn btn-warning btn-rounded btn-sm" onclick="$.form.reset()"><i class="fa fa-refresh"></i>&nbsp;重置</a>
</li>
</ul>
</div>
</form>
</div>
<div class="col-sm-12 select-table table-striped">
<table id="bootstrap-table"></table>
</div>
</div>
</div>
<th:block th:include="include :: footer" />
<script th:inline="javascript">
var unbindFlag = [[${@permission.hasPermi('adam:caomei:passport:edit')}]];
var detailFlag = [[${@permission.hasPermi('adam:caomei:passport:list')}]];
var prefix = ctx + "adam/caomei/passport";
$(function() {
var options = {
url: prefix + "/list",
formId: "formId",
modalName: "护照",
columns: [
{ field: 'mid', title: '主键', visible: false },
{ field: 'passportNo', title: '护照ID' },
{
field: 'nickname',
title: '关联用户',
formatter: function(value, row, index) {
if (!value && row.bindStatus !== 1) {
return '<span class="text-muted">未绑定用户</span>';
}
return value || '-';
}
},
{ field: 'realName', title: '姓名', formatter: function(v) { return v || '-'; } },
{ field: 'mobile', title: '手机号', formatter: function(v) { return v || '-'; } },
{ field: 'idCard', title: '证件号', formatter: function(v) { return v || '-'; } },
{
field: 'bindStatus',
title: '绑定状态',
align: 'center',
formatter: function(value) {
if (value === 1) {
return '<span class="label label-info">已绑定</span>';
}
if (value === 0) {
return '<span class="label label-default">未绑定</span>';
}
if (value === 2) {
return '<span class="label label-warning">已作废</span>';
}
return value;
}
},
{ field: 'claimedBadgeCount', title: '已领取徽章数', align: 'center' },
{
title: '操作',
align: 'center',
formatter: function(value, row) {
var actions = [];
actions.push('<a class="btn btn-info btn-xs ' + detailFlag + '" href="javascript:void(0)" onclick=\'openDetail(' + JSON.stringify(row.passportNo) + ')\'><i class="fa fa-eye"></i>查看详情</a> ');
if (row.bindStatus === 1) {
actions.push('<a class="btn btn-warning btn-xs ' + unbindFlag + '" href="javascript:void(0)" onclick=\'unbind(' + JSON.stringify(row.passportNo) + ')\'><i class="fa fa-unlink"></i>解绑用户</a>');
}
return actions.join('');
}
}
]
};
$.table.init(options);
setTimeout(function() {
renderBoundPassportCountToolbar();
loadBoundPassportCount();
}, 0);
});
function renderBoundPassportCountToolbar() {
if ($("#boundPassportCountBox").length > 0) {
return;
}
var html = '' +
'<div id="boundPassportCountBox" class="pull-left" style="margin-top: 10px; margin-right: 8px;">' +
' <span style="display: inline-block; line-height: 30px; margin-right: 6px;">已绑定护照数量:<strong id="boundPassportCount">加载中...</strong></span>' +
' <button type="button" class="btn btn-default btn-outline" onclick="loadBoundPassportCount()" title="刷新已绑定护照数量">' +
' <i class="fa fa-refresh"></i>' +
' </button>' +
'</div>';
var $toolbar = $(".fixed-table-toolbar").first();
if ($toolbar.length > 0) {
$toolbar.prepend(html);
}
}
function loadBoundPassportCount() {
$.get(prefix + "/bound/count", function(res) {
if (res && res.code === 0) {
$("#boundPassportCount").text(res.data || 0);
} else {
$("#boundPassportCount").text("-");
}
});
}
function openDetail(passportNo) {
$.modal.openTab("护照详情", prefix + "/detail/" + encodeURIComponent(passportNo));
}
function unbind(passportNo) {
$.modal.confirm("解绑后该护照将变为未绑定状态,用户需重新绑定。确认解绑?", function() {
$.operate.post(prefix + "/unbind", { passportNo: passportNo });
});
}
</script>
</body>
</html>
package com.liquidnet.client.admin.zhengzai.adam.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.github.pagehelper.PageInfo;
import com.liquidnet.service.adam.dto.param.AdamCaomeiBadgeClaimUserSearchParam;
import com.liquidnet.service.adam.dto.param.AdamCaomeiBadgeSearchParam;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiBadgeClaimUserVo;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiBadgeVo;
import com.liquidnet.service.adam.entity.AdamCaomeiBadge;
/**
* 草莓护照-徽章管理
*/
public interface IAdamCaomeiBadgeAdminService extends IService<AdamCaomeiBadge> {
/**
* 徽章管理(每个徽章领取人数)
* @param param
* @return
*/
PageInfo<AdamCaomeiBadgeVo> listWithClaimedCount(AdamCaomeiBadgeSearchParam param);
/**
* 徽章领取用户列表
* @param param
* @return
*/
PageInfo<AdamCaomeiBadgeClaimUserVo> listClaimUsers(AdamCaomeiBadgeClaimUserSearchParam param);
/**
* 清除用户端「上架草莓徽章列表」Redis 缓存,与 adam 侧 {@code AdamRdmService#delPublishedCaomeiBadges} 一致。
* 后台新增/编辑/上下架成功后应调用,避免用户端仍读到旧列表。
*/
void delPublishedCaomeiBadges();
/**
* kylin 演出是否存在(performances_id)
*/
boolean kylinPerformanceExists(String performancesId);
/**
* 是否存在与名称相同的其他徽章(trim 后精确匹配)
*
* @param name 徽章名称
* @param excludeBadgeId 编辑时排除当前徽章 ID;新增传 null 或空
*/
boolean existsOtherBadgeWithSameName(String name, String excludeBadgeId);
}
package com.liquidnet.client.admin.zhengzai.adam.service;
import com.github.pagehelper.PageInfo;
import com.liquidnet.service.adam.dto.param.AdamCaomeiBadgeApplyAuditSearchParam;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiBadgeApplyAuditDetailVo;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiBadgeApplyAuditVo;
public interface IAdamCaomeiBadgeApplyAuditAdminService {
PageInfo<AdamCaomeiBadgeApplyAuditVo> listApplyAudits(AdamCaomeiBadgeApplyAuditSearchParam param);
AdamCaomeiBadgeApplyAuditDetailVo getApplyAuditDetail(String applyRecordId);
boolean passApplyAudit(String applyRecordId);
boolean rejectApplyAudit(String applyRecordId, String rejectReason);
}
package com.liquidnet.client.admin.zhengzai.adam.service;
import com.github.pagehelper.PageInfo;
import com.liquidnet.service.adam.dto.param.AdamCaomeiPassportSearchParam;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiPassportDetailVo;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiPassportListVo;
/**
* 草莓护照-护照管理
*/
public interface IAdamCaomeiPassportAdminService {
PageInfo<AdamCaomeiPassportListVo> listPassports(AdamCaomeiPassportSearchParam param);
int countBoundPassports();
AdamCaomeiPassportDetailVo getPassportDetail(String passportNo);
boolean unbindPassport(String passportNo);
}
package com.liquidnet.client.admin.zhengzai.adam.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.PageInfo;
import com.liquidnet.client.admin.zhengzai.adam.service.IAdamCaomeiBadgeAdminService;
import com.liquidnet.common.cache.redis.util.RedisDataSourceUtil;
import com.liquidnet.service.adam.constant.AdamRedisConst;
import com.liquidnet.service.adam.dto.AdamCaomeiBadgeClaimCountDto;
import com.liquidnet.service.adam.dto.AdamCaomeiBadgeClaimUserDto;
import com.liquidnet.service.adam.dto.param.AdamCaomeiBadgeClaimUserSearchParam;
import com.liquidnet.service.adam.dto.param.AdamCaomeiBadgeSearchParam;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiBadgeClaimUserVo;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiBadgeVo;
import com.liquidnet.service.adam.entity.AdamCaomeiBadge;
import com.liquidnet.service.adam.mapper.AdamCaomeiBadgeMapper;
import com.liquidnet.service.kylin.entity.KylinPerformances;
import com.liquidnet.service.kylin.mapper.KylinPerformancesMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.sql.Timestamp;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Service
public class AdamCaomeiBadgeAdminServiceImpl extends ServiceImpl<AdamCaomeiBadgeMapper, AdamCaomeiBadge> implements IAdamCaomeiBadgeAdminService {
@Autowired
private RedisDataSourceUtil redisDataSourceUtil;
@Autowired
private KylinPerformancesMapper kylinPerformancesMapper;
@Override
public void delPublishedCaomeiBadges() {
redisDataSourceUtil.getRedisAdamUtil().del(AdamRedisConst.INFO_CAOMEI_BADGE_PUBLISHED);
}
@Override
public boolean save(AdamCaomeiBadge entity) {
boolean ok = super.save(entity);
if (ok) {
delPublishedCaomeiBadges();
}
return ok;
}
@Override
public boolean updateById(AdamCaomeiBadge entity) {
boolean ok = super.updateById(entity);
if (ok) {
delPublishedCaomeiBadges();
}
return ok;
}
@Override
public boolean kylinPerformanceExists(String performancesId) {
String id = StringUtils.trimToEmpty(performancesId);
if (StringUtils.isBlank(id)) {
return false;
}
int c = kylinPerformancesMapper.selectCount(
Wrappers.lambdaQuery(KylinPerformances.class).eq(KylinPerformances::getPerformancesId, id)
);
return c > 0;
}
@Override
public boolean existsOtherBadgeWithSameName(String name, String excludeBadgeId) {
String n = StringUtils.trimToEmpty(name);
if (StringUtils.isBlank(n)) {
return false;
}
LambdaQueryWrapper<AdamCaomeiBadge> w = Wrappers.lambdaQuery(AdamCaomeiBadge.class).eq(AdamCaomeiBadge::getName, n);
if (StringUtils.isNotBlank(excludeBadgeId)) {
w.ne(AdamCaomeiBadge::getBadgeId, excludeBadgeId.trim());
}
return this.count(w) > 0;
}
@Override
public PageInfo<AdamCaomeiBadgeVo> listWithClaimedCount(AdamCaomeiBadgeSearchParam param) {
LambdaQueryWrapper<AdamCaomeiBadge> queryWrapper = Wrappers.lambdaQuery(AdamCaomeiBadge.class);
if (StringUtils.isNotBlank(param.getName())) {
queryWrapper.like(AdamCaomeiBadge::getName, param.getName());
}
if (param.getType() != null) {
queryWrapper.eq(AdamCaomeiBadge::getType, param.getType());
}
if (param.getDisplayStatus() != null) {
queryWrapper.eq(AdamCaomeiBadge::getDisplayStatus, param.getDisplayStatus());
}
if (StringUtils.isNotBlank(param.getCreatedAtBegin())) {
try {
LocalDate d = LocalDate.parse(param.getCreatedAtBegin().trim());
queryWrapper.ge(AdamCaomeiBadge::getCreatedAt, Timestamp.valueOf(d.atStartOfDay()));
} catch (DateTimeParseException e) {
log.warn("invalid createdAtBegin: {}", param.getCreatedAtBegin());
}
}
if (StringUtils.isNotBlank(param.getCreatedAtEnd())) {
try {
LocalDate d = LocalDate.parse(param.getCreatedAtEnd().trim());
queryWrapper.le(AdamCaomeiBadge::getCreatedAt, Timestamp.valueOf(d.atTime(23, 59, 59)));
} catch (DateTimeParseException e) {
log.warn("invalid createdAtEnd: {}", param.getCreatedAtEnd());
}
}
queryWrapper.orderByDesc(AdamCaomeiBadge::getSort, AdamCaomeiBadge::getUpdatedAt);
List<AdamCaomeiBadge> badges = this.list(queryWrapper);
PageInfo<AdamCaomeiBadge> entityPage = new PageInfo<>(badges);
List<String> badgeIds = entityPage.getList().stream()
.map(AdamCaomeiBadge::getBadgeId)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toList());
// key: 徽章ID,value: 领取人数
Map<String, Integer> countMap = badgeIds.isEmpty() ? Collections.emptyMap() :
baseMapper.selectClaimedCountByBadgeIds(badgeIds).stream()
.collect(Collectors.toMap(AdamCaomeiBadgeClaimCountDto::getBadgeId, AdamCaomeiBadgeClaimCountDto::getClaimedCount, (a, b) -> a));
List<AdamCaomeiBadgeVo> voList = entityPage.getList().stream().map(item -> {
AdamCaomeiBadgeVo vo = new AdamCaomeiBadgeVo();
BeanUtils.copyProperties(item, vo);
vo.setClaimedCount(countMap.getOrDefault(item.getBadgeId(), 0));
return vo;
}).collect(Collectors.toList());
PageInfo<AdamCaomeiBadgeVo> voPage = new PageInfo<>(voList);
voPage.setTotal(entityPage.getTotal());
return voPage;
}
@Override
public PageInfo<AdamCaomeiBadgeClaimUserVo> listClaimUsers(AdamCaomeiBadgeClaimUserSearchParam param) {
if (param == null || StringUtils.isBlank(param.getBadgeId())) {
return new PageInfo<>(Collections.emptyList());
}
List<AdamCaomeiBadgeClaimUserDto> list = baseMapper.selectClaimUsersByBadgeId(param.getBadgeId());
PageInfo<AdamCaomeiBadgeClaimUserDto> dtoPage = new PageInfo<>(list);
List<AdamCaomeiBadgeClaimUserVo> voList = dtoPage.getList().stream().map(item -> {
AdamCaomeiBadgeClaimUserVo vo = new AdamCaomeiBadgeClaimUserVo();
BeanUtils.copyProperties(item, vo);
return vo;
}).collect(Collectors.toList());
PageInfo<AdamCaomeiBadgeClaimUserVo> voPage = new PageInfo<>(voList);
voPage.setTotal(dtoPage.getTotal());
return voPage;
}
}
package com.liquidnet.client.admin.zhengzai.adam.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.github.pagehelper.PageInfo;
import com.liquidnet.client.admin.zhengzai.adam.service.IAdamCaomeiBadgeApplyAuditAdminService;
import com.liquidnet.client.admin.zhengzai.adam.util.CaomeiBadgeApplyProofUrls;
import com.liquidnet.service.adam.dto.AdamCaomeiBadgeApplyAuditDetailDto;
import com.liquidnet.service.adam.dto.AdamCaomeiBadgeApplyAuditDto;
import com.liquidnet.service.adam.dto.param.AdamCaomeiBadgeApplyAuditSearchParam;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiBadgeApplyAuditDetailVo;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiBadgeApplyAuditVo;
import com.liquidnet.service.adam.entity.AdamCaomeiBadgeApplyRecord;
import com.liquidnet.service.adam.mapper.AdamCaomeiBadgeApplyRecordMapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class AdamCaomeiBadgeApplyAuditAdminServiceImpl implements IAdamCaomeiBadgeApplyAuditAdminService {
@Autowired
private AdamCaomeiBadgeApplyRecordMapper badgeApplyRecordMapper;
@Override
public PageInfo<AdamCaomeiBadgeApplyAuditVo> listApplyAudits(AdamCaomeiBadgeApplyAuditSearchParam param) {
if (param == null) {
param = new AdamCaomeiBadgeApplyAuditSearchParam();
}
List<AdamCaomeiBadgeApplyAuditDto> rows = badgeApplyRecordMapper.selectAuditList(
StringUtils.trimToEmpty(param.getUserName()),
param.getAuditStatus()
);
PageInfo<AdamCaomeiBadgeApplyAuditDto> dtoPage = new PageInfo<>(rows);
List<AdamCaomeiBadgeApplyAuditVo> voList = dtoPage.getList().stream().map(item -> {
AdamCaomeiBadgeApplyAuditVo vo = new AdamCaomeiBadgeApplyAuditVo();
BeanUtils.copyProperties(item, vo);
vo.setIdCard(maskIdCard(item.getIdCard()));
vo.setProofImageUrls(CaomeiBadgeApplyProofUrls.parse(item.getProofImageUrl()));
return vo;
}).collect(Collectors.toList());
PageInfo<AdamCaomeiBadgeApplyAuditVo> voPage = new PageInfo<>(voList);
voPage.setTotal(dtoPage.getTotal());
return voPage;
}
@Override
public AdamCaomeiBadgeApplyAuditDetailVo getApplyAuditDetail(String applyRecordId) {
if (StringUtils.isBlank(applyRecordId)) {
return null;
}
AdamCaomeiBadgeApplyAuditDetailDto detail = badgeApplyRecordMapper.selectAuditDetailByApplyRecordId(applyRecordId.trim());
if (detail == null) {
return null;
}
AdamCaomeiBadgeApplyAuditDetailVo vo = new AdamCaomeiBadgeApplyAuditDetailVo();
BeanUtils.copyProperties(detail, vo);
vo.setIdCard(maskIdCard(detail.getIdCard()));
vo.setProofImageUrls(CaomeiBadgeApplyProofUrls.parse(detail.getProofImageUrl()));
return vo;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean passApplyAudit(String applyRecordId) {
if (StringUtils.isBlank(applyRecordId)) {
return false;
}
AdamCaomeiBadgeApplyRecord record = badgeApplyRecordMapper.selectOne(
Wrappers.lambdaQuery(AdamCaomeiBadgeApplyRecord.class)
.eq(AdamCaomeiBadgeApplyRecord::getApplyRecordId, applyRecordId.trim())
.last("limit 1")
);
if (record == null || record.getAuditStatus() == null || record.getAuditStatus() != 0) {
return false;
}
if (StringUtils.isBlank(record.getUserId()) || StringUtils.isBlank(record.getBadgeId())) {
return false;
}
int updated = badgeApplyRecordMapper.passAudit(applyRecordId.trim());
if (updated <= 0) {
return false;
}
return true;
}
@Override
public boolean rejectApplyAudit(String applyRecordId, String rejectReason) {
if (StringUtils.isBlank(applyRecordId) || StringUtils.isBlank(rejectReason)) {
return false;
}
return badgeApplyRecordMapper.rejectAudit(applyRecordId.trim(), rejectReason.trim()) > 0;
}
private static String maskIdCard(String idCard) {
if (StringUtils.isBlank(idCard)) {
// 未实名或无证件信息
return "";
}
if (idCard.length() > 8) {
return idCard.substring(0, 4) + "****" + idCard.substring(idCard.length() - 4);
}
return "****";
}
}
package com.liquidnet.client.admin.zhengzai.adam.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.github.pagehelper.PageInfo;
import com.liquidnet.client.admin.zhengzai.adam.service.IAdamCaomeiPassportAdminService;
import com.liquidnet.common.cache.redis.util.RedisDataSourceUtil;
import com.liquidnet.service.adam.constant.AdamRedisConst;
import com.liquidnet.service.adam.dto.AdamCaomeiPassportListDto;
import com.liquidnet.service.adam.dto.AdamCaomeiPassportUserBadgeDto;
import com.liquidnet.service.adam.dto.param.AdamCaomeiPassportSearchParam;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiPassportBadgeItemVo;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiPassportDetailVo;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiPassportListVo;
import com.liquidnet.service.adam.entity.AdamCaomeiPassport;
import com.liquidnet.service.adam.entity.AdamRealName;
import com.liquidnet.service.adam.entity.AdamUser;
import com.liquidnet.service.adam.entity.AdamUserInfo;
import com.liquidnet.service.adam.mapper.AdamCaomeiBadgeMapper;
import com.liquidnet.service.adam.mapper.AdamCaomeiPassportMapper;
import com.liquidnet.service.adam.mapper.AdamRealNameMapper;
import com.liquidnet.service.adam.mapper.AdamUserInfoMapper;
import com.liquidnet.service.adam.mapper.AdamUserMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
public class AdamCaomeiPassportAdminServiceImpl implements IAdamCaomeiPassportAdminService {
@Autowired
private AdamCaomeiPassportMapper adamCaomeiPassportMapper;
@Autowired
private AdamUserMapper adamUserMapper;
@Autowired
private AdamUserInfoMapper adamUserInfoMapper;
@Autowired
private AdamRealNameMapper adamRealNameMapper;
@Autowired
private AdamCaomeiBadgeMapper adamCaomeiBadgeMapper;
@Autowired
private RedisDataSourceUtil redisDataSourceUtil;
@Override
public PageInfo<AdamCaomeiPassportListVo> listPassports(AdamCaomeiPassportSearchParam param) {
if (param == null) {
param = new AdamCaomeiPassportSearchParam();
}
List<AdamCaomeiPassportListDto> rows = adamCaomeiPassportMapper.selectPassportAdminList(
param.getPassportNo(),
param.getUserName(),
param.getMobile(),
param.getIdCard(),
param.getBindStatus()
);
PageInfo<AdamCaomeiPassportListDto> dtoPage = new PageInfo<>(rows);
List<AdamCaomeiPassportListVo> voList = dtoPage.getList().stream().map(d -> {
AdamCaomeiPassportListVo vo = new AdamCaomeiPassportListVo();
BeanUtils.copyProperties(d, vo);
return vo;
}).collect(Collectors.toList());
PageInfo<AdamCaomeiPassportListVo> voPage = new PageInfo<>(voList);
voPage.setTotal(dtoPage.getTotal());
return voPage;
}
@Override
public int countBoundPassports() {
return adamCaomeiPassportMapper.countBoundPassports();
}
@Override
public AdamCaomeiPassportDetailVo getPassportDetail(String passportNo) {
if (StringUtils.isBlank(passportNo)) {
return null;
}
AdamCaomeiPassport passport = adamCaomeiPassportMapper.selectOne(
Wrappers.lambdaQuery(AdamCaomeiPassport.class)
.eq(AdamCaomeiPassport::getPassportNo, passportNo.trim())
);
if (passport == null) {
return null;
}
AdamCaomeiPassportDetailVo vo = new AdamCaomeiPassportDetailVo();
vo.setMid(passport.getMid());
vo.setPassportNo(passport.getPassportNo());
vo.setStatus(passport.getStatus());
vo.setUserId(passport.getUserId());
vo.setBoundAt(passport.getBoundAt());
vo.setBatchNo(passport.getBatchNo());
String uid = passport.getUserId();
if (StringUtils.isBlank(uid)) {
vo.setBadges(Collections.emptyList());
return vo;
}
AdamUser user = adamUserMapper.selectOne(Wrappers.lambdaQuery(AdamUser.class).eq(AdamUser::getUid, uid));
AdamUserInfo userInfo = adamUserInfoMapper.selectOne(Wrappers.lambdaQuery(AdamUserInfo.class).eq(AdamUserInfo::getUid, uid));
vo.setNickname(userInfo != null ? StringUtils.defaultString(userInfo.getNickname()) : "");
vo.setMobile(user != null ? maskMobile(user.getMobile()) : "");
AdamRealName realName = adamRealNameMapper.selectOne(
Wrappers.lambdaQuery(AdamRealName.class)
.eq(AdamRealName::getUid, uid)
.eq(AdamRealName::getState, 1)
.orderByDesc(AdamRealName::getMid)
.last("limit 1")
);
if (realName != null) {
vo.setRealName(StringUtils.defaultString(realName.getName()));
vo.setIdCard(maskIdCard(realName.getIdCard()));
} else {
vo.setRealName("");
vo.setIdCard("");
}
List<AdamCaomeiPassportUserBadgeDto> badgeRows = adamCaomeiPassportMapper.selectUserBadgesByUid(uid);
vo.setBadges(badgeRows.stream().map(b -> {
AdamCaomeiPassportBadgeItemVo item = new AdamCaomeiPassportBadgeItemVo();
item.setBadgeId(b.getBadgeId());
item.setBadgeName(b.getBadgeName());
item.setIcon(b.getIcon());
item.setClaimedAt(b.getClaimedAt());
item.setSource(b.getSource());
return item;
}).collect(Collectors.toList()));
return vo;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean unbindPassport(String passportNo) {
if (StringUtils.isBlank(passportNo)) {
return false;
}
AdamCaomeiPassport passport = adamCaomeiPassportMapper.selectOne(
Wrappers.lambdaQuery(AdamCaomeiPassport.class)
.eq(AdamCaomeiPassport::getPassportNo, passportNo.trim())
);
if (passport == null) {
return false;
}
if (passport.getStatus() == null || passport.getStatus() != 1) {
return false;
}
String uid = passport.getUserId();
Date now = new Date();
passport.setStatus(0);
passport.setUserId("");
passport.setUnboundAt(now);
passport.setUpdatedAt(now);
boolean updated = adamCaomeiPassportMapper.updateById(passport) > 0;
if (!updated) {
return false;
}
if (StringUtils.isNotBlank(uid)) {
// 解绑后,清理该用户由“绑定护照”自动发放的护照纪念徽章
adamCaomeiBadgeMapper.deletePassportTypeBadgesByUid(uid);
// 清理用户草莓徽章缓存,避免前端短时间读到解绑前数据
redisDataSourceUtil.getRedisAdamUtil().del(AdamRedisConst.INFO_CAOMEI_BADGE_USER.concat(uid));
}
return true;
}
private static String maskMobile(String mobile) {
if (StringUtils.isBlank(mobile)) {
return "";
}
if (mobile.length() >= 11) {
return mobile.substring(0, 3) + "****" + mobile.substring(7);
}
return mobile;
}
private static String maskIdCard(String idCard) {
if (StringUtils.isBlank(idCard)) {
return "";
}
if (idCard.length() > 8) {
return idCard.substring(0, 4) + "****" + idCard.substring(idCard.length() - 4);
}
return "****";
}
}
package com.liquidnet.client.admin.zhengzai.adam.util;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* 补签申请附件 URL 解析(支持逗号/分号/换行分隔或 JSON 字符串数组)。
*/
public final class CaomeiBadgeApplyProofUrls {
private CaomeiBadgeApplyProofUrls() {
}
public static List<String> parse(String raw) {
if (StringUtils.isBlank(raw)) {
return Collections.emptyList();
}
String t = raw.trim();
if (t.startsWith("[") && t.endsWith("]")) {
try {
JSONArray arr = JSON.parseArray(t);
if (arr != null && !arr.isEmpty()) {
Set<String> seen = new LinkedHashSet<>();
for (int i = 0; i < arr.size(); i++) {
String u = StringUtils.trimToEmpty(arr.getString(i));
if (StringUtils.isNotBlank(u)) {
seen.add(u);
}
}
return new ArrayList<>(seen);
}
} catch (Exception ignored) {
// fall through to delimiter split
}
}
String[] parts = t.split("[,;\\n]+");
List<String> out = new ArrayList<>();
for (String p : parts) {
String u = StringUtils.trimToEmpty(p);
if (StringUtils.isNotBlank(u)) {
out.add(u);
}
}
return out;
}
}
......@@ -71,4 +71,7 @@ public class RedisKeyExpireConst {
* 收钱吧下单防重锁过期时间 (10秒)
*/
public static final long SQB_ORDER_LOCK_EXPIRE = 10;
// 已发布状态徽章过期时间:30天
public static final long CAOMEI_BADGE_PUBLISHED_EXPIRE = 30 * 24 * 60 * 60;
}
......@@ -87,6 +87,18 @@ spring:
max-wait: -1
max-idle: 8
min-idle: 0
kylin:
database: ${liquidnet.redis.kylin.database}
dbs: ${liquidnet.redis.kylin.dbs}
port: ${liquidnet.redis.kylin.port}
host: ${liquidnet.redis.kylin.host}
password: ${liquidnet.redis.kylin.password}
lettuce:
pool:
max-active: 16
max-wait: -1
max-idle: 8
min-idle: 4
database: 15
dbs: ${liquidnet.redis.adam.dbs}
port: ${liquidnet.redis.adam.port}
......
package com.liquidnet.service.adam.dto;
import lombok.Data;
import java.util.Date;
@Data
public class AdamCaomeiBadgeApplyAuditDetailDto {
private String applyRecordId;
private String userId;
private String nickname;
private String realName;
private String idCard;
private String badgeId;
private String badgeName;
private String performanceId;
private String performanceName;
private String proofImageUrl;
private Integer auditStatus;
private String rejectReason;
private Date createdAt;
private Date updatedAt;
}
package com.liquidnet.service.adam.dto;
import lombok.Data;
import java.util.Date;
@Data
public class AdamCaomeiBadgeApplyAuditDto {
private String applyRecordId;
private String userId;
private String nickname;
private String realName;
private String idCard;
private String badgeName;
private String performanceName;
private String proofImageUrl;
private Integer auditStatus;
private Date createdAt;
}
package com.liquidnet.service.adam.dto;
import lombok.Data;
import java.util.Date;
@Data
public class AdamCaomeiBadgeApplyRecordUserDto {
private String applyRecordId;
private String badgeName;
private Date applyTime;
private Integer auditStatus;
private String rejectReason;
private String proofImageUrl;
}
package com.liquidnet.service.adam.dto;
import lombok.Data;
@Data
public class AdamCaomeiBadgeClaimCountDto {
private String badgeId;
private Integer claimedCount;
}
package com.liquidnet.service.adam.dto;
import lombok.Data;
import java.util.Date;
@Data
public class AdamCaomeiBadgeClaimUserDto {
private String nickname;
private String mobile;
private Date claimedAt;
private Integer source;
}
package com.liquidnet.service.adam.dto;
import lombok.Data;
/**
* 护照管理列表行
*/
@Data
public class AdamCaomeiPassportListDto {
private Long mid;
private String passportNo;
private String nickname;
private String realName;
private String mobile;
private String idCard;
/** 0-未绑定 1-已绑定 2-已作废 */
private Integer bindStatus;
private Integer claimedBadgeCount;
}
package com.liquidnet.service.adam.dto;
import lombok.Data;
import java.util.Date;
@Data
public class AdamCaomeiPassportUserBadgeDto {
private String badgeId;
private String badgeName;
private String subTitle;
private String icon;
private String shareText;
private Integer type;
private String performanceId;
private Date claimedAt;
private Integer source;
}
package com.liquidnet.service.adam.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 草莓护照-徽章配置表
*/
@Data
@TableName("adam_caomei_badge")
public class AdamCaomeiBadge implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "mid", type = IdType.AUTO)
private Long mid;
/**
* 徽章ID
*/
private String badgeId;
/**
* 徽章名称
*/
private String name;
/**
* 徽章副标题(最多10字)
*/
private String subTitle;
/**
* 徽章图标 (Emoji字符或图片URL)
*/
private String icon;
/**
* 徽章类型: 1-护照纪念徽章, 2-演出纪念徽章, 3-特殊徽章
*/
private Integer type;
/**
* 关联演出ID (仅演出纪念徽章必填,其他类型为空)
*/
private String performanceId;
/**
* 上架状态: 0-下架(默认), 1-已发布
*/
private Integer displayStatus;
/**
* 排序值,数值越大越靠前
*/
private Integer sort;
/**
* 徽章分享文案
*/
private String shareText;
/**
* 添加时间
*/
private Date createdAt;
/**
* 更新时间
*/
private Date updatedAt;
}
package com.liquidnet.service.adam.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
@TableName("adam_caomei_badge_apply_record")
public class AdamCaomeiBadgeApplyRecord implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "mid", type = IdType.AUTO)
private Long mid;
private String applyRecordId;
private String userId;
private String badgeId;
private String performanceId;
private String proofImageUrl;
private Integer auditStatus;
private String rejectReason;
private Date createdAt;
private Date updatedAt;
}
package com.liquidnet.service.adam.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 草莓护照-实体编号表
*/
@Data
@TableName("adam_caomei_passport")
public class AdamCaomeiPassport implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "mid", type = IdType.AUTO)
private Long mid;
private String passportNo;
/**
* 绑定状态: 0-未绑定, 1-已绑定, 2-已作废
*/
private Integer status;
private String userId;
private Date boundAt;
private Date unboundAt;
private String batchNo;
private String remark;
private Date createdAt;
private Date updatedAt;
}
package com.liquidnet.service.adam.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.liquidnet.service.adam.dto.AdamCaomeiBadgeApplyAuditDetailDto;
import com.liquidnet.service.adam.dto.AdamCaomeiBadgeApplyAuditDto;
import com.liquidnet.service.adam.dto.AdamCaomeiBadgeApplyRecordUserDto;
import com.liquidnet.service.adam.entity.AdamCaomeiBadgeApplyRecord;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
public interface AdamCaomeiBadgeApplyRecordMapper extends BaseMapper<AdamCaomeiBadgeApplyRecord> {
@Select({
"<script>",
"select ",
"ar.apply_record_id as applyRecordId,",
"ifnull(u.mobile, '') as userId,",
"ifnull(ui.nickname, '') as nickname,",
"ifnull(arn.name, '') as realName,",
"ifnull(arn.id_card, '') as idCard,",
"ifnull(b.name, '') as badgeName,",
"ifnull(kp.title, ar.performance_id) as performanceName,",
"ar.proof_image_url as proofImageUrl,",
"ar.audit_status as auditStatus,",
"ar.created_at as createdAt ",
"from adam_caomei_badge_apply_record ar ",
"left join adam_user u on ar.user_id = u.uid ",
"left join adam_user_info ui on ar.user_id = ui.uid ",
"left join (",
" select r.uid, r.name, r.id_card ",
" from adam_real_name r ",
" inner join (",
" select uid, max(mid) as max_mid from adam_real_name where state = 1 group by uid",
" ) rm on r.uid = rm.uid and r.mid = rm.max_mid and r.state = 1",
") arn on ar.user_id = arn.uid ",
"left join adam_caomei_badge b on ar.badge_id = b.badge_id ",
"left join kylin_performances kp on ar.performance_id = kp.performances_id ",
"where 1 = 1 ",
"<if test='userName != null and userName != \"\"'>",
"and (ui.nickname like concat('%', #{userName}, '%') or arn.name like concat('%', #{userName}, '%')) ",
"</if>",
"<if test='auditStatus != null'>",
"and ar.audit_status = #{auditStatus} ",
"</if>",
"order by case when ar.audit_status = 0 then 0 else 1 end asc, ar.created_at desc",
"</script>"
})
List<AdamCaomeiBadgeApplyAuditDto> selectAuditList(@Param("userName") String userName,
@Param("auditStatus") Integer auditStatus);
@Select({
"select ",
"ar.apply_record_id as applyRecordId,",
"ifnull(u.mobile, '') as userId,",
"ifnull(ui.nickname, '') as nickname,",
"ifnull(arn.name, '') as realName,",
"ifnull(arn.id_card, '') as idCard,",
"ar.badge_id as badgeId,",
"ifnull(b.name, '') as badgeName,",
"ar.performance_id as performanceId,",
"ifnull(kp.title, ar.performance_id) as performanceName,",
"ar.proof_image_url as proofImageUrl,",
"ar.audit_status as auditStatus,",
"ifnull(ar.reject_reason, '') as rejectReason,",
"ar.created_at as createdAt,",
"ar.updated_at as updatedAt ",
"from adam_caomei_badge_apply_record ar ",
"left join adam_user u on ar.user_id = u.uid ",
"left join adam_user_info ui on ar.user_id = ui.uid ",
"left join (",
" select r.uid, r.name, r.id_card ",
" from adam_real_name r ",
" inner join (select uid, max(mid) as max_mid from adam_real_name where state = 1 group by uid) rm ",
" on r.uid = rm.uid and r.mid = rm.max_mid and r.state = 1",
") arn on ar.user_id = arn.uid ",
"left join adam_caomei_badge b on ar.badge_id = b.badge_id ",
"left join kylin_performances kp on ar.performance_id = kp.performances_id ",
"where ar.apply_record_id = #{applyRecordId} ",
"limit 1"
})
AdamCaomeiBadgeApplyAuditDetailDto selectAuditDetailByApplyRecordId(@Param("applyRecordId") String applyRecordId);
@Update({
"update adam_caomei_badge_apply_record ",
"set audit_status = 1, reject_reason = null, updated_at = now() ",
"where apply_record_id = #{applyRecordId} and audit_status = 0"
})
int passAudit(@Param("applyRecordId") String applyRecordId);
@Update({
"update adam_caomei_badge_apply_record ",
"set audit_status = 2, reject_reason = #{rejectReason}, updated_at = now() ",
"where apply_record_id = #{applyRecordId} and audit_status = 0"
})
int rejectAudit(@Param("applyRecordId") String applyRecordId, @Param("rejectReason") String rejectReason);
@Insert({
"insert ignore into adam_caomei_user_badge (user_id, badge_id, source, created_at) ",
"values (#{userId}, #{badgeId}, 3, now())"
})
int issueBadgeIfAbsent(@Param("userId") String userId, @Param("badgeId") String badgeId);
@Select({
"select ",
"ar.apply_record_id as applyRecordId,",
"ifnull(b.name, '') as badgeName,",
"ar.created_at as applyTime,",
"ar.audit_status as auditStatus,",
"ifnull(ar.reject_reason, '') as rejectReason,",
"proof_image_url as proofImageUrl",
"from adam_caomei_badge_apply_record ar ",
"left join adam_caomei_badge b on ar.badge_id = b.badge_id ",
"where ar.user_id = #{uid} ",
"order by ar.created_at desc"
})
List<AdamCaomeiBadgeApplyRecordUserDto> selectUserApplyRecordsByUid(@Param("uid") String uid);
}
package com.liquidnet.service.adam.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
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;
import java.util.Date;
import java.util.List;
public interface AdamCaomeiBadgeMapper extends BaseMapper<AdamCaomeiBadge> {
@Select({
"<script>",
"select badge_id as badgeId, count(1) as claimedCount",
"from adam_caomei_user_badge",
"where badge_id in",
"<foreach collection='badgeIds' item='badgeId' open='(' separator=',' close=')'>",
"#{badgeId}",
"</foreach>",
"group by badge_id",
"</script>"
})
List<AdamCaomeiBadgeClaimCountDto> selectClaimedCountByBadgeIds(@Param("badgeIds") List<String> badgeIds);
@Select({
"select ",
"ifnull(ui.nickname,'') as nickname,",
"case ",
"when u.mobile is null then '' ",
"when length(u.mobile) >= 11 then concat(substring(u.mobile,1,3),'****',substring(u.mobile,8,4)) ",
"else u.mobile end as mobile,",
"ub.created_at as claimedAt,",
"ub.source as source ",
"from adam_caomei_user_badge ub ",
"left join adam_user_info ui on ub.user_id = ui.uid ",
"left join adam_user u on ub.user_id = u.uid ",
"where ub.badge_id = #{badgeId} ",
"order by ub.created_at desc"
})
List<AdamCaomeiBadgeClaimUserDto> selectClaimUsersByBadgeId(@Param("badgeId") String badgeId);
@Delete({
"delete ub from adam_caomei_user_badge ub ",
"inner join adam_caomei_badge b on ub.badge_id = b.badge_id ",
"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);
@Insert({
"<script>",
"insert ignore into adam_caomei_user_badge (user_id, badge_id, source, created_at) values ",
"<foreach collection='badgeIds' item='badgeId' separator=','>",
"(#{userId}, #{badgeId}, #{source}, #{createdAt})",
"</foreach>",
"</script>"
})
int insertUserBadgesBatch(@Param("userId") String userId,
@Param("badgeIds") List<String> badgeIds,
@Param("source") Integer source,
@Param("createdAt") Date createdAt);
/**
* 根据身份证号查询用户已支付的演出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);
@Select("select count(1) from adam_caomei_user_badge where user_id = #{userId} and badge_id = #{badgeId}")
int checkUserBadgeExists(@Param("userId") String userId, @Param("badgeId") String badgeId);
@Select({
"<script>",
"select badge_id from adam_caomei_user_badge",
"where user_id = #{userId} and badge_id in",
"<foreach collection='badgeIds' item='badgeId' open='(' separator=',' close=')'>",
"#{badgeId}",
"</foreach>",
"</script>"
})
List<String> selectClaimedBadgeIdsByUserAndBadgeIds(@Param("userId") String userId,
@Param("badgeIds") List<String> badgeIds);
@Insert({
"insert into adam_caomei_user_badge (user_id, badge_id, source, created_at) ",
"values (#{userId}, #{badgeId}, #{source}, now())"
})
int insertUserBadge(@Param("userId") String userId, @Param("badgeId") String badgeId, @Param("source") Integer source);
}
package com.liquidnet.service.adam.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
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.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
public interface AdamCaomeiPassportMapper extends BaseMapper<AdamCaomeiPassport> {
List<AdamCaomeiPassportListDto> selectPassportAdminList(@Param("passportNo") String passportNo,
@Param("userName") String userName,
@Param("mobile") String mobile,
@Param("idCard") String idCard,
@Param("bindStatus") Integer bindStatus);
@Select("select count(1) from adam_caomei_passport where status = 1")
int countBoundPassports();
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);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.liquidnet.service.adam.mapper.AdamCaomeiPassportMapper">
<select id="selectPassportAdminList" resultType="com.liquidnet.service.adam.dto.AdamCaomeiPassportListDto">
SELECT
p.mid AS mid,
p.passport_no AS passportNo,
IFNULL(ui.nickname, '') AS nickname,
IFNULL(arn.name, '') AS realName,
CASE
WHEN u.mobile IS NULL OR u.mobile = '' THEN ''
WHEN CHAR_LENGTH(u.mobile) &gt;= 11 THEN CONCAT(SUBSTRING(u.mobile, 1, 3), '****', SUBSTRING(u.mobile, 8, 4))
ELSE u.mobile
END AS mobile,
CASE
WHEN arn.id_card IS NULL OR arn.id_card = '' THEN ''
WHEN CHAR_LENGTH(arn.id_card) &gt; 8 THEN CONCAT(SUBSTRING(arn.id_card, 1, 4), '****', SUBSTRING(arn.id_card, CHAR_LENGTH(arn.id_card) - 3, 4))
ELSE '****'
END AS idCard,
p.status AS bindStatus,
IFNULL(bc.cnt, 0) AS claimedBadgeCount
FROM adam_caomei_passport p
LEFT JOIN adam_user u ON p.user_id = u.uid AND p.user_id &lt;&gt; ''
LEFT JOIN adam_user_info ui ON p.user_id = ui.uid
LEFT JOIN (
SELECT r.uid, r.name, r.id_card
FROM adam_real_name r
INNER JOIN (
SELECT uid, MAX(mid) AS max_mid
FROM adam_real_name
WHERE state = 1
GROUP BY uid
) rm ON r.uid = rm.uid AND r.mid = rm.max_mid AND r.state = 1
) arn ON p.user_id = arn.uid
LEFT JOIN (
SELECT user_id, COUNT(1) AS cnt
FROM adam_caomei_user_badge
GROUP BY user_id
) bc ON p.user_id = bc.user_id
<where>
<if test="passportNo != null and passportNo != ''">
AND p.passport_no LIKE CONCAT('%', #{passportNo}, '%')
</if>
<if test="userName != null and userName != ''">
AND (ui.nickname LIKE CONCAT('%', #{userName}, '%') OR arn.name LIKE CONCAT('%', #{userName}, '%'))
</if>
<if test="mobile != null and mobile != ''">
AND u.mobile LIKE CONCAT('%', #{mobile}, '%')
</if>
<if test="idCard != null and idCard != ''">
AND arn.id_card LIKE CONCAT('%', #{idCard}, '%')
</if>
<if test="bindStatus != null">
AND p.status = #{bindStatus}
</if>
</where>
<!-- 已绑定优先展示,避免仅按更新时间导致已绑定记录长期沉底 -->
ORDER BY (p.status = 1) DESC, p.updated_at DESC, p.mid DESC
</select>
<select id="selectUserBadgesByUid" resultType="com.liquidnet.service.adam.dto.AdamCaomeiPassportUserBadgeDto">
SELECT
ub.badge_id AS badgeId,
IFNULL(b.name, '') AS badgeName,
IFNULL(b.sub_title, '') AS subTitle,
IFNULL(b.icon, '') AS icon,
IFNULL(b.share_text, '') AS shareText,
IFNULL(b.type, 0) AS type,
IFNULL(b.performance_id, '') AS performanceId,
ub.created_at AS claimedAt,
ub.source AS source
FROM adam_caomei_user_badge ub
LEFT JOIN adam_caomei_badge b ON ub.badge_id = b.badge_id
WHERE ub.user_id = #{uid}
ORDER BY ub.created_at DESC
</select>
</mapper>
......@@ -23,6 +23,11 @@
<groupId>com.liquidnet</groupId>
<artifactId>liquidnet-common-web</artifactId>
</dependency>
<dependency>
<groupId>com.liquidnet</groupId>
<artifactId>liquidnet-common-cache-redis</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.liquidnet</groupId>-->
<!-- <artifactId>liquidnet-common-cache-redisson</artifactId>-->
......@@ -38,11 +43,11 @@
<artifactId>liquidnet-service-adam-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.liquidnet</groupId>-->
<!-- <artifactId>liquidnet-service-kylin-api</artifactId>-->
<!-- <version>1.0-SNAPSHOT</version>-->
<!-- </dependency>-->
<dependency>
<groupId>com.liquidnet</groupId>
<artifactId>liquidnet-service-kylin-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.liquidnet</groupId>
<artifactId>liquidnet-common-third-easemob</artifactId>
......
package com.liquidnet.service.adam.controller;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.liquidnet.commons.lang.util.CurrentUtil;
import com.liquidnet.service.adam.dto.param.AdamCaomeiBadgeApplyParam;
import com.liquidnet.service.adam.dto.param.AdamCaomeiBadgeClaimParam;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiBadgeApplyRecordUserVo;
import com.liquidnet.service.adam.service.IAdamCaomeiBadgeUserService;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;
import javax.validation.Valid;
import java.util.List;
@ApiSupport(order = 10046)
@Api(tags = "草莓徽章(用户端)")
@Slf4j
@Validated
@RestController
@RequestMapping("caomei/badge")
public class AdamCaomeiBadgeUserController {
@Autowired
private IAdamCaomeiBadgeUserService adamCaomeiBadgeUserService;
@ApiOperationSupport(order = 1)
@ApiOperation("认领徽章")
@PostMapping("claim")
public ResponseDto<List<String>> claim(@Valid @RequestBody AdamCaomeiBadgeClaimParam param) {
String uid = CurrentUtil.getCurrentUid();
return adamCaomeiBadgeUserService.claimBadges(param.getBadgeIds(), uid);
}
@ApiOperationSupport(order = 2)
@ApiOperation("补签记录列表")
@GetMapping("apply/list")
public ResponseDto<List<AdamCaomeiBadgeApplyRecordUserVo>> applyList() {
String uid = CurrentUtil.getCurrentUid();
return adamCaomeiBadgeUserService.getApplyRecords(uid);
}
@ApiOperationSupport(order = 3)
@ApiOperation("发起补签申请(驳回后可重新上传)")
@PostMapping("apply")
public ResponseDto<String> apply(@Valid @RequestBody AdamCaomeiBadgeApplyParam param) {
String uid = CurrentUtil.getCurrentUid();
return adamCaomeiBadgeUserService.applyBadge(param, uid);
}
}
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.dto.vo.AdamCaomeiPassportUserClaimedBadgeVo;
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;
import java.util.List;
@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<List<AdamCaomeiPassportUserClaimedBadgeVo>> bind(@Valid @RequestBody AdamCaomeiPassportNoParam param) {
return adamCaomeiPassportUserService.bindPassport(param.getPassportNo());
}
@ApiOperationSupport(order = 2)
@ApiOperation("护照首页聚合数据")
@GetMapping("home")
public ResponseDto<AdamCaomeiPassportHomeVo> home() {
return adamCaomeiPassportUserService.getPassportHome();
}
@ApiOperationSupport(order = 3)
@ApiOperation("校验当前用户是否已绑定护照")
@GetMapping("/check")
public ResponseDto<Boolean> checkBound() {
return adamCaomeiPassportUserService.checkPassportBound();
}
}
......@@ -12,6 +12,7 @@ import com.liquidnet.commons.lang.util.SensitizeUtil;
import com.liquidnet.service.adam.constant.AdamConst;
import com.liquidnet.service.adam.dto.AdamThirdPartParam;
import com.liquidnet.service.adam.dto.AdamUserInfoParam;
import com.liquidnet.service.adam.dto.param.AdamIdentityParam;
import com.liquidnet.service.adam.dto.vo.*;
import com.liquidnet.service.adam.service.AdamRdmService;
import com.liquidnet.service.adam.service.IAdamUserInfoService;
......@@ -299,16 +300,15 @@ public class AdamUserController {
}
String currentUid = CurrentUtil.getCurrentUid();
AdamRealInfoVo realInfoVoByUid = adamRdmService.getRealInfoVoByUidPlain(currentUid);
// 已实名 && 三要素通过
if (null != realInfoVoByUid && realInfoVoByUid.getNode() == 3) {
// 用户输入信息与三要素通过的信息相符,则认证通过
if ((realInfoVoByUid.getName().concat(realInfoVoByUid.getIdCard())).equals(name.concat(idCard))) {
realInfoVoByUid.setName(SensitizeUtil.chineseName(realInfoVoByUid.getName()));
realInfoVoByUid.setIdCard(SensitizeUtil.custom(realInfoVoByUid.getIdCard(), 3, 2));
return ResponseDto.success(realInfoVoByUid);
} else {
return ResponseDto.failure(ErrorMapping.get("10113"));
}
// 已实名:无论二要素(node=2)还是三要素(node=3),都视为已完成实名,直接返回(避免重复实名/跨入口重复认证)
if (null != realInfoVoByUid && realInfoVoByUid.getState() != null && realInfoVoByUid.getState() == 1) {
realInfoVoByUid.setName(SensitizeUtil.chineseName(realInfoVoByUid.getName()));
realInfoVoByUid.setIdCard(SensitizeUtil.custom(realInfoVoByUid.getIdCard(), 3, 2));
return ResponseDto.success(realInfoVoByUid);
}
ResponseDto<AdamRealInfoVo> guard = guardIdCardNotBoundToOtherUid(currentUid, idCard);
if (guard != null) {
return guard;
}
AdamRealInfoVo vo = adamUserService.identityForUpsert(currentUid, name, idCard, (String) CurrentUtil.getTokenClaims().get(CurrentUtil.TOKEN_MOBILE), true);
vo.setName(SensitizeUtil.chineseName(vo.getName()));
......@@ -503,7 +503,8 @@ public class AdamUserController {
return ResponseDto.failure(ErrorMapping.get("10114"));
}
AdamRealInfoVo realInfoVoByUidPlain = adamRdmService.getRealInfoVoByUidPlain(uid);
if (null == realInfoVoByUidPlain || 3 != realInfoVoByUidPlain.getNode()) {
// 已实名:兼容二要素(node=2) 与 三要素(node=3),统一以 state=1 为准
if (null == realInfoVoByUidPlain || realInfoVoByUidPlain.getState() == null || realInfoVoByUidPlain.getState() != 1) {
return ResponseDto.failure(ErrorMapping.get("10115"));
}
AdamUserIdentityInfoVo userIdentityInfoVo = AdamUserIdentityInfoVo.getNew();
......@@ -535,6 +536,90 @@ public class AdamUserController {
return ResponseDto.success(adamRdmService.getUserInfoVoByUid(uid));
}
@ApiOperationSupport(order = 15)
@ApiOperation(value = "实名认证V2(二要素)", notes = "支持手动填写(name+idCard) 或 选择观演人(entersId)")
@PostMapping(value = {"identity/v2"})
public ResponseDto<AdamRealInfoVo> identityV2(@RequestBody AdamIdentityParam param) {
String currentUid = CurrentUtil.getCurrentUid();
// 1. 检查当前登录用户是否已实名
AdamRealInfoVo realInfoVo = adamRdmService.getRealInfoVoByUidPlain(currentUid);
if (realInfoVo != null && realInfoVo.getState() != null && realInfoVo.getState() == 1) {
// 已实名,直接返回
realInfoVo.setName(SensitizeUtil.chineseName(realInfoVo.getName()));
realInfoVo.setIdCard(SensitizeUtil.custom(realInfoVo.getIdCard(), 3, 2));
return ResponseDto.success(realInfoVo);
}
String name = param.getName();
String idCard = param.getIdCard();
// 2. 如果传了 entersId,则根据 entersId 获取观演人的姓名和身份证号
if (StringUtils.isNotBlank(param.getEntersId())) {
List<AdamEntersVo> entersList = adamRdmService.getEntersVoByUid(currentUid);
if (!CollectionUtils.isEmpty(entersList)) {
AdamEntersVo entersVo = entersList.stream()
.filter(e -> param.getEntersId().equals(e.getEntersId()))
.findFirst()
.orElse(null);
if (entersVo == null) {
return ResponseDto.failure(ErrorMapping.get("10105")); // 观演人不存在
}
// 只同步身份证类型的观演人信息 (type=1 大陆身份证)
if (entersVo.getType() == null || entersVo.getType() != 1) {
return ResponseDto.failure("10101", "仅支持大陆身份证类型的观演人进行实名认证");
}
name = entersVo.getName();
idCard = entersVo.getIdCard();
} else {
return ResponseDto.failure(ErrorMapping.get("10105")); // 观演人不存在
}
}
// 3. 校验姓名和身份证号不能为空
if (StringUtils.isBlank(name) || StringUtils.isBlank(idCard)) {
return ResponseDto.failure(ErrorMapping.get("10101")); // 姓名或身份证件号无效
}
// 4. 格式校验
if (!java.util.regex.Pattern.matches(LnsRegex.Valid.CN_HANZI, name)) {
return ResponseDto.failure(ErrorMapping.get("10103"));
}
if (!java.util.regex.Pattern.matches(LnsRegex.Valid.CN_ID_CARD_REF, idCard)) {
return ResponseDto.failure(ErrorMapping.get("10104"));
}
ResponseDto<AdamRealInfoVo> guard = guardIdCardNotBoundToOtherUid(currentUid, idCard);
if (guard != null) {
return guard;
}
AdamRealInfoVo vo = adamUserService.verifyTwoElements(currentUid, name, idCard);
if (null == vo) {
log.error("[identityV2] 二要素认证失败, param: {}", JsonUtils.toJson(param));
return ResponseDto.failure(ErrorMapping.get("10101"));
}
vo.setName(SensitizeUtil.chineseName(vo.getName()));
vo.setIdCard(SensitizeUtil.custom(vo.getIdCard(), 3, 2));
return ResponseDto.success(vo);
}
/**
* 同一个身份证号(state=1)只能绑定一个账号。
* 放在 controller 层:对所有实名入口统一拦截,返回明确错误码给前端。
*/
private ResponseDto<AdamRealInfoVo> guardIdCardNotBoundToOtherUid(String uid, String idCard) {
if (StringUtils.isBlank(uid) || StringUtils.isBlank(idCard)) {
return null;
}
String boundUid = adamRdmService.getRealNameBoundUidByIdCard(idCard);
if (StringUtils.isNotBlank(boundUid) && !uid.equals(boundUid)) {
return ResponseDto.failure(ErrorMapping.get("10614"));
}
return null;
}
/* ---------------------------- Internal Method ---------------------------- */
private static final String PHP_API_SMS_CODE_VALID = "/smsValidation";
......
......@@ -2,23 +2,34 @@ package com.liquidnet.service.adam.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.fasterxml.jackson.databind.JsonNode;
import com.liquidnet.common.cache.redis.util.RedisDataSourceUtil;
import com.liquidnet.common.cache.redis.util.RedisUtil;
import com.liquidnet.common.exception.LiquidnetServiceException;
import com.liquidnet.commons.lang.util.IdentityUtils;
import com.liquidnet.commons.lang.util.JsonUtils;
import com.liquidnet.commons.lang.util.SensitizeUtil;
import com.liquidnet.service.adam.constant.AdamRedisConst;
import com.liquidnet.service.adam.dto.AdamCaomeiPassportUserBadgeDto;
import com.liquidnet.service.adam.dto.AdamUserInfoDto;
import com.liquidnet.service.adam.dto.vo.*;
import com.liquidnet.service.adam.entity.AdamCaomeiBadge;
import com.liquidnet.service.adam.entity.AdamEnters;
import com.liquidnet.service.adam.entity.AdamRealName;
import com.liquidnet.service.adam.entity.AdamUserMember;
import com.liquidnet.service.kylin.constant.KylinRedisConst;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinPerformanceVo;
import com.liquidnet.service.adam.mapper.AdamCaomeiBadgeMapper;
import com.liquidnet.service.adam.mapper.AdamCaomeiPassportMapper;
import com.liquidnet.service.adam.mapper.AdamEntersMapper;
import com.liquidnet.service.adam.mapper.AdamRealNameMapper;
import com.liquidnet.service.adam.mapper.AdamUserMapper;
import com.liquidnet.service.adam.mapper.AdamUserMemberMapper;
import com.liquidnet.service.adam.util.ObjectUtil;
import com.liquidnet.service.base.ErrorMapping;
import com.liquidnet.service.base.constant.RedisKeyExpireConst;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
......@@ -26,6 +37,7 @@ import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
......@@ -40,12 +52,21 @@ public class AdamRdmService {
@Autowired
RedisUtil redisUtil;
@Autowired
RedisDataSourceUtil redisDataSourceUtil;
@Autowired
AdamUserMapper adamUserMapper;
@Autowired
AdamUserMemberMapper adamUserMemberMapper;
@Autowired
AdamRealNameMapper adamRealNameMapper;
@Autowired
AdamEntersMapper adamEntersMapper;
@Autowired
AdamCaomeiBadgeMapper adamCaomeiBadgeMapper;
@Autowired
AdamCaomeiPassportMapper adamCaomeiPassportMapper;
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | Switch config */
......@@ -257,6 +278,50 @@ public class AdamRdmService {
redisUtil.del(AdamRedisConst.INFO_REAL_NAME.concat(uid));
}
/**
* 根据身份证号查询已实名绑定的 uid(优先 Redis,未命中回源 MySQL 并回写 Redis)
* 返回空表示未绑定或不可用。
*/
public String getRealNameBoundUidByIdCard(String idCard) {
if (StringUtils.isEmpty(idCard)) {
return "";
}
String key = AdamRedisConst.INFO_REAL_NAME_UID_BY_IDCARD.concat(idCard);
try {
String uid = (String) redisUtil.get(key);
if (!StringUtils.isEmpty(uid)) {
return uid;
}
} catch (Exception e) {
log.warn("[getRealNameBoundUidByIdCard] redis get failed, key: {}", key, e);
}
// 回源 MySQL:取任意一个 state=1 的 uid(同证件号按规则应唯一)
try {
AdamRealName r = adamRealNameMapper.selectOne(
Wrappers.lambdaQuery(AdamRealName.class)
.eq(AdamRealName::getState, 1)
.eq(AdamRealName::getType, 1)
.eq(AdamRealName::getIdCard, idCard)
.orderByDesc(AdamRealName::getMid)
.last("limit 1")
);
String uid = r != null ? org.springframework.util.StringUtils.trimWhitespace(r.getUid()) : "";
if (!StringUtils.isEmpty(uid)) {
try {
// 30天过期:防止长期脏数据无法自愈;真实解绑/注销可再补 delete
redisUtil.set(key, uid, 60L * 60 * 24 * 30);
} catch (Exception e) {
log.warn("[getRealNameBoundUidByIdCard] redis set failed, key: {}", key, e);
}
}
return uid;
} catch (Exception e) {
log.error("[getRealNameBoundUidByIdCard] mysql fallback failed, idCard: {}", idCard, e);
// 降级:查询失败直接放行(由上层决定是否继续阻断)
return "";
}
}
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | 认证失败的<ID_TYPE+ID_NO, ID_NAME> */
public boolean setCertificationJunk(int idType, String idNo, String idName) {
......@@ -630,6 +695,197 @@ public class AdamRdmService {
redisUtil.del(AdamRedisConst.INFO_ADDRESSES.concat(uid));
}
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | 草莓徽章缓存 */
public boolean setPublishedCaomeiBadges(List<AdamCaomeiBadge> badges) {
return redisUtil.set(AdamRedisConst.INFO_CAOMEI_BADGE_PUBLISHED, badges, RedisKeyExpireConst.CAOMEI_BADGE_PUBLISHED_EXPIRE);
}
/**
* 获取所有已上架的徽章列表
* @return
*/
public List<AdamCaomeiBadge> getPublishedCaomeiBadges() {
String rk = AdamRedisConst.INFO_CAOMEI_BADGE_PUBLISHED;
List<AdamCaomeiBadge> badges = (List<AdamCaomeiBadge>) redisUtil.get(rk);
if (CollectionUtils.isEmpty(badges)) {
badges = adamCaomeiBadgeMapper.selectList(
new QueryWrapper<AdamCaomeiBadge>().lambda()
.eq(AdamCaomeiBadge::getDisplayStatus, 1)
.orderByAsc(AdamCaomeiBadge::getType)
.orderByDesc(AdamCaomeiBadge::getSort)
.orderByDesc(AdamCaomeiBadge::getMid)
);
if (!CollectionUtils.isEmpty(badges)) {
setPublishedCaomeiBadges(badges);
}
}
return badges;
}
public void delPublishedCaomeiBadges() {
redisUtil.del(AdamRedisConst.INFO_CAOMEI_BADGE_PUBLISHED);
}
/**
* 设置用户领取徽章redis
* @param uid
* @param badges
* @return
*/
public boolean setUserCaomeiBadgesByUid(String uid, List<AdamCaomeiPassportUserBadgeDto> badges) {
return redisUtil.set(AdamRedisConst.INFO_CAOMEI_BADGE_USER.concat(uid), badges, 300);
}
/**
* 获取用户徽章
* @param uid
* @return
*/
public List<AdamCaomeiPassportUserBadgeDto> getUserCaomeiBadgesByUid(String uid) {
String rk = AdamRedisConst.INFO_CAOMEI_BADGE_USER.concat(uid);
List<AdamCaomeiPassportUserBadgeDto> badges = (List<AdamCaomeiPassportUserBadgeDto>) redisUtil.get(rk);
if (CollectionUtils.isEmpty(badges)) {
badges = adamCaomeiPassportMapper.selectUserBadgesByUid(uid);
if (!CollectionUtils.isEmpty(badges)) {
setUserCaomeiBadgesByUid(uid, badges);
}
}
return badges;
}
/**
* 用户认领徽章时追加一条(与收货地址 addAddressesVoByUid 相同:写回整列表到 Redis)
*/
public boolean addUserCaomeiBadgeDtoByUid(String uid,
List<AdamCaomeiPassportUserBadgeDto> vos,
AdamCaomeiPassportUserBadgeDto dto) {
if (vos == null) {
vos = new ArrayList<>();
}
vos.add(dto);
return setUserCaomeiBadgesByUid(uid, vos);
}
/**
* 用户认领徽章时批量追加(一次 set 回 Redis,避免循环多次写 Redis)
*/
public boolean addUserCaomeiBadgeDtosByUid(String uid,
List<AdamCaomeiPassportUserBadgeDto> vos,
List<AdamCaomeiPassportUserBadgeDto> appendVos) {
if (vos == null) {
vos = new ArrayList<>();
}
if (CollectionUtils.isEmpty(appendVos)) {
return setUserCaomeiBadgesByUid(uid, vos);
}
vos.addAll(appendVos);
return setUserCaomeiBadgesByUid(uid, vos);
}
/**
* 根据身份证获取已支付演出ID列表(60秒短缓存,缓解无索引热点查询)
*/
public List<String> getPaidPerformanceIdsByIdCard(String idCard) {
if (StringUtils.isEmpty(idCard)) {
return Collections.emptyList();
}
String rk = AdamRedisConst.INFO_CAOMEI_PAID_PERFORMANCE_IDS_BY_IDCARD.concat(idCard);
List<String> performanceIds = (List<String>) redisUtil.get(rk);
if (CollectionUtils.isEmpty(performanceIds)) {
performanceIds = adamCaomeiBadgeMapper.selectPaidPerformanceIdsByIdCard(idCard);
if (performanceIds == null) {
performanceIds = Collections.emptyList();
}
redisUtil.set(rk, performanceIds, 60);
}
return performanceIds;
}
/**
* 从演出缓存(kylin redis)读取演出标题,用于草莓护照演出徽章分组展示。
* 读取不到时返回 null。
*/
public String getPerformanceTitleById(String performanceId) {
try {
KylinPerformanceVo vo = getKylinPerformanceVoById(performanceId);
if (vo != null) {
return vo.getTitle();
}
return null;
} catch (Exception e) {
log.warn("[getPerformanceTitleById] 读取演出缓存失败, performanceId: {}", performanceId, e);
return null;
}
}
/**
* 从演出缓存读取开演/结束时间(yyyyMMddHHmmss),用于草莓徽章补签自动审核。
* 读取不到时返回 null。
*/
public PerformanceTimeRange getPerformanceTimeRangeById(String performanceId) {
try {
KylinPerformanceVo vo = getKylinPerformanceVoById(performanceId);
if (vo == null) {
return null;
}
String start = vo.getTimeStart();
String end = vo.getTimeEnd();
LocalDateTime st = parseKylinTime(start);
LocalDateTime et = parseKylinTime(end);
if (st == null || et == null) {
return null;
}
return new PerformanceTimeRange(st, et);
} catch (Exception e) {
log.warn("[getPerformanceTimeRangeById] 读取演出时间失败, performanceId: {}", performanceId, e);
return null;
}
}
/**
* 统一从 kylin redis 缓存读取演出 VO。
* 缓存若因序列化差异读成 Map,则尽量转换成 {@link KylinPerformanceVo} 后返回。
*/
public KylinPerformanceVo getKylinPerformanceVoById(String performanceId) {
return (KylinPerformanceVo) redisDataSourceUtil.getRedisKylinUtil().get(KylinRedisConst.PERFORMANCES + performanceId);
}
private static final DateTimeFormatter KYLIN_TIME_COMPACT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
private static final DateTimeFormatter KYLIN_TIME_DASH = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static LocalDateTime parseKylinTime(String v) {
if (StringUtils.isEmpty(v)) {
return null;
}
String t = v.trim();
// 优先 kylin 常见格式:yyyyMMddHHmmss
try {
if (t.length() >= 14) {
return LocalDateTime.parse(t.substring(0, 14), KYLIN_TIME_COMPACT);
}
} catch (Exception ignored) { }
// 兼容:yyyy-MM-dd HH:mm:ss
try {
if (t.length() >= 19) {
return LocalDateTime.parse(t.substring(0, 19), KYLIN_TIME_DASH);
}
} catch (Exception ignored) { }
return null;
}
@Getter
public static final class PerformanceTimeRange {
private final LocalDateTime start;
private final LocalDateTime end;
public PerformanceTimeRange(LocalDateTime start, LocalDateTime end) {
this.start = start;
this.end = end;
}
}
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | List<购买会员黑名单UID> */
public boolean setBlacklistForMember(List<String> uids) {
......
package com.liquidnet.service.adam.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.liquidnet.commons.lang.util.IDGenerator;
import com.liquidnet.service.adam.dto.AdamCaomeiPassportUserBadgeDto;
import com.liquidnet.service.adam.dto.param.AdamCaomeiBadgeApplyParam;
import com.liquidnet.service.adam.dto.AdamCaomeiBadgeApplyRecordUserDto;
import com.liquidnet.service.adam.dto.vo.AdamCaomeiBadgeApplyRecordUserVo;
import com.liquidnet.service.adam.dto.vo.AdamRealInfoVo;
import com.liquidnet.service.adam.entity.AdamCaomeiBadgeApplyRecord;
import com.liquidnet.service.adam.entity.AdamCaomeiBadge;
import com.liquidnet.service.adam.entity.AdamCaomeiPassport;
import com.liquidnet.service.adam.mapper.AdamCaomeiBadgeApplyRecordMapper;
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.IAdamCaomeiBadgeUserService;
import com.liquidnet.service.adam.util.QueueUtils;
import com.liquidnet.service.base.ErrorMapping;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.base.SqlMapping;
import com.liquidnet.service.base.constant.MQConst;
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.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.stream.Collectors;
@Slf4j
@Service
public class AdamCaomeiBadgeUserServiceImpl implements IAdamCaomeiBadgeUserService {
@Autowired
private AdamCaomeiBadgeMapper adamCaomeiBadgeMapper;
@Autowired
private AdamCaomeiPassportMapper adamCaomeiPassportMapper;
@Autowired
private AdamCaomeiBadgeApplyRecordMapper badgeApplyRecordMapper;
@Autowired
private AdamRdmService adamRdmService;
@Autowired
private QueueUtils queueUtils;
@Override
@Transactional(rollbackFor = Exception.class)
public ResponseDto<List<String>> claimBadges(List<String> badgeIds, String uid) {
if (StringUtils.isBlank(uid)) {
return ResponseDto.failure(ErrorMapping.get("10001"));
}
if (badgeIds == null || badgeIds.isEmpty()) {
return ResponseDto.failure("徽章ID列表不能为空");
}
List<String> requestBadgeIds = badgeIds.stream()
.filter(StringUtils::isNotBlank)
.map(StringUtils::trim)
.collect(Collectors.collectingAndThen(Collectors.toCollection(LinkedHashSet::new), ArrayList::new));
if (requestBadgeIds.isEmpty()) {
return ResponseDto.failure("徽章ID列表不能为空");
}
// 1) 批量查询徽章配置(一次查询)
List<AdamCaomeiBadge> badges = adamCaomeiBadgeMapper.selectList(
Wrappers.lambdaQuery(AdamCaomeiBadge.class)
.in(AdamCaomeiBadge::getBadgeId, requestBadgeIds)
.eq(AdamCaomeiBadge::getDisplayStatus, 1)
);
if (badges == null || badges.size() != requestBadgeIds.size()) {
log.error("[claimBadges] 存在徽章不存在或未上架, uid: {}, badgeIds: {}", uid, requestBadgeIds);
return ResponseDto.failure(ErrorMapping.get("10607"));
}
Map<String, AdamCaomeiBadge> badgeMap = badges.stream()
.collect(Collectors.toMap(AdamCaomeiBadge::getBadgeId, b -> b));
List<AdamCaomeiBadge> orderedBadges = requestBadgeIds.stream()
.map(badgeMap::get)
.collect(Collectors.toList());
// 2. 与收货地址 add 一致:先取 Redis(或回源)用户徽章列表,再判重;DB 再兜一层防缓存未命中或其它写入路径
List<AdamCaomeiPassportUserBadgeDto> badgeVos = adamRdmService.getUserCaomeiBadgesByUid(uid);
if (badgeVos == null) {
badgeVos = new ArrayList<>();
} else {
badgeVos = new ArrayList<>(badgeVos);
}
Set<String> claimedInCache = badgeVos.stream()
.map(AdamCaomeiPassportUserBadgeDto::getBadgeId)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toSet());
if (requestBadgeIds.stream().anyMatch(claimedInCache::contains)) {
log.info("[claimBadges] 用户已领取过徽章(Redis列表), uid: {}, badgeIds: {}", uid, requestBadgeIds);
return ResponseDto.failure(ErrorMapping.get("10608"));
}
List<String> claimedInDb = adamCaomeiBadgeMapper.selectClaimedBadgeIdsByUserAndBadgeIds(uid, requestBadgeIds);
if (claimedInDb != null && !claimedInDb.isEmpty()) {
log.info("[claimBadges] 用户已领取过徽章(DB), uid: {}, badgeIds: {}", uid, claimedInDb);
return ResponseDto.failure(ErrorMapping.get("10608"));
}
boolean hasType1 = orderedBadges.stream().anyMatch(b -> b.getType() != null && b.getType() == 1);
boolean hasType2 = orderedBadges.stream().anyMatch(b -> b.getType() != null && b.getType() == 2);
boolean hasType3 = orderedBadges.stream().anyMatch(b -> b.getType() != null && b.getType() == 3);
boolean hasUnknownType = orderedBadges.stream().anyMatch(b -> b.getType() == null || (b.getType() != 1 && b.getType() != 2 && b.getType() != 3));
if (hasType3) {
log.error("[claimBadges] 含特殊徽章不可自助领取, uid: {}, badgeIds: {}", uid, requestBadgeIds);
return ResponseDto.failure(ErrorMapping.get("10612"));
}
if (hasUnknownType) {
log.error("[claimBadges] 含未知徽章类型, uid: {}, badgeIds: {}", uid, requestBadgeIds);
return ResponseDto.failure(ErrorMapping.get("10613"));
}
if (hasType1) {
AdamCaomeiPassport bound = adamCaomeiPassportMapper.selectOne(
Wrappers.lambdaQuery(AdamCaomeiPassport.class)
.eq(AdamCaomeiPassport::getUserId, uid)
.eq(AdamCaomeiPassport::getStatus, 1)
.last("limit 1")
);
if (bound == null) {
log.error("[claimBadges] 认领护照纪念徽章需先绑定护照, uid: {}, badgeIds: {}", uid, requestBadgeIds);
return ResponseDto.failure(ErrorMapping.get("10609"));
}
}
Set<String> paidPerformanceSet = Collections.emptySet();
Set<String> passedApplyBadgeIds = Collections.emptySet();
Set<String> passedApplyPerformanceIds = Collections.emptySet();
Map<String, Set<String>> perfAllBadgeIds = Collections.emptyMap();
if (hasType2) {
AdamRealInfoVo real = adamRdmService.getRealInfoVoByUidPlain(uid);
if (real == null || real.getState() == null || real.getState() != 1 || StringUtils.isBlank(real.getIdCard())) {
log.error("[claimBadges] 认领演出徽章需先实名, uid: {}, badgeIds: {}", uid, requestBadgeIds);
return ResponseDto.failure(ErrorMapping.get("10610"));
}
List<String> paidPerformanceIds = adamRdmService.getPaidPerformanceIdsByIdCard(real.getIdCard());
paidPerformanceSet = paidPerformanceIds == null ? Collections.emptySet() : new HashSet<>(paidPerformanceIds);
List<AdamCaomeiBadgeApplyRecord> passedApplyRecords = badgeApplyRecordMapper.selectList(
Wrappers.lambdaQuery(AdamCaomeiBadgeApplyRecord.class)
.eq(AdamCaomeiBadgeApplyRecord::getUserId, uid)
.eq(AdamCaomeiBadgeApplyRecord::getAuditStatus, 1)
);
passedApplyBadgeIds = new HashSet<>();
passedApplyPerformanceIds = new HashSet<>();
if (passedApplyRecords != null) {
for (AdamCaomeiBadgeApplyRecord r : passedApplyRecords) {
if (r == null) {
continue;
}
if (StringUtils.isNotBlank(r.getBadgeId())) {
passedApplyBadgeIds.add(r.getBadgeId());
}
if (StringUtils.isNotBlank(r.getPerformanceId())) {
passedApplyPerformanceIds.add(r.getPerformanceId());
}
}
}
Set<String> targetPerfIds = orderedBadges.stream()
.filter(b -> b.getType() != null && b.getType() == 2)
.map(AdamCaomeiBadge::getPerformanceId)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toSet());
if (!targetPerfIds.isEmpty()) {
List<AdamCaomeiBadge> perfBadges = adamCaomeiBadgeMapper.selectList(
Wrappers.lambdaQuery(AdamCaomeiBadge.class)
.eq(AdamCaomeiBadge::getType, 2)
.in(AdamCaomeiBadge::getPerformanceId, targetPerfIds)
);
perfAllBadgeIds = perfBadges.stream()
.filter(b -> StringUtils.isNotBlank(b.getPerformanceId()) && StringUtils.isNotBlank(b.getBadgeId()))
.collect(Collectors.groupingBy(AdamCaomeiBadge::getPerformanceId,
Collectors.mapping(AdamCaomeiBadge::getBadgeId, Collectors.toSet())));
}
}
List<String> claimedBadgeIds = new ArrayList<>();
for (AdamCaomeiBadge badge : orderedBadges) {
int type = badge.getType() == null ? 0 : badge.getType();
int source = 1;
if (type == 2) {
String perfId = StringUtils.trimToEmpty(badge.getPerformanceId());
boolean hasPaidRecord = StringUtils.isNotBlank(perfId) && paidPerformanceSet.contains(perfId);
boolean hasPassedApply = false;
if (StringUtils.isNotBlank(perfId)) {
if (passedApplyPerformanceIds.contains(perfId)) {
hasPassedApply = true;
} else {
Set<String> badgeIdSet = perfAllBadgeIds.getOrDefault(perfId, Collections.emptySet());
hasPassedApply = badgeIdSet.stream().anyMatch(passedApplyBadgeIds::contains);
}
}
if (!hasPaidRecord && !hasPassedApply) {
log.error("[claimBadges] 无购票记录且无通过补签,无法认领, uid: {}, badgeId: {}", uid, badge.getBadgeId());
return ResponseDto.failure(ErrorMapping.get("10611"));
}
source = hasPaidRecord ? 2 : 3;
}
grantUserBadgeRedisThenMq(uid, badge, source, badgeVos);
claimedBadgeIds.add(badge.getBadgeId());
}
log.info("[claimBadges] 批量认领成功, uid: {}, badgeIds: {}", uid, claimedBadgeIds);
return ResponseDto.success(claimedBadgeIds);
}
/**
* Redis 追加用户徽章展示 DTO,再发 MQ 异步执行 sqlmap 中的 INSERT。
*/
private void grantUserBadgeRedisThenMq(String uid, AdamCaomeiBadge badge, int source,
List<AdamCaomeiPassportUserBadgeDto> badgeVos) {
Date now = new Date();
AdamCaomeiPassportUserBadgeDto dto = new AdamCaomeiPassportUserBadgeDto();
dto.setBadgeId(badge.getBadgeId());
dto.setBadgeName(StringUtils.defaultString(badge.getName()));
dto.setIcon(StringUtils.defaultString(badge.getIcon()));
dto.setShareText(StringUtils.defaultString(badge.getShareText()));
dto.setType(badge.getType());
dto.setClaimedAt(now);
dto.setSource(source);
adamRdmService.addUserCaomeiBadgeDtoByUid(uid, badgeVos, dto);
long t = System.currentTimeMillis();
queueUtils.sendMsgByRedis(
MQConst.AdamQueue.SQL_UCENTER.getKey(),
SqlMapping.get("adam_caomei_user_badge.add", uid, badge.getBadgeId(), source, now)
);
log.debug("[claimBadge] MQ耗时:{}ms, uid: {}, badgeId: {}", System.currentTimeMillis() - t, uid, badge.getBadgeId());
}
@Override
public ResponseDto<List<AdamCaomeiBadgeApplyRecordUserVo>> getApplyRecords(String uid) {
if (StringUtils.isBlank(uid)) {
return ResponseDto.failure(ErrorMapping.get("10001"));
}
List<AdamCaomeiBadgeApplyRecordUserDto> rows = badgeApplyRecordMapper.selectUserApplyRecordsByUid(uid);
if (rows == null) {
rows = Collections.emptyList();
}
List<AdamCaomeiBadgeApplyRecordUserVo> list = rows.stream().map(r -> {
AdamCaomeiBadgeApplyRecordUserVo v = new AdamCaomeiBadgeApplyRecordUserVo();
v.setApplyRecordId(r.getApplyRecordId());
v.setBadgeName(StringUtils.defaultString(r.getBadgeName()));
v.setApplyTime(r.getApplyTime());
v.setAuditStatus(r.getAuditStatus());
v.setRejectReason(StringUtils.defaultString(r.getRejectReason()));
v.setCanReupload(r.getAuditStatus() != null && r.getAuditStatus() == 2);
v.setProofImageUrl(r.getProofImageUrl());
return v;
}).collect(Collectors.toList());
return ResponseDto.success(list);
}
@Override
public ResponseDto<String> applyBadge(AdamCaomeiBadgeApplyParam param, String uid) {
if (StringUtils.isBlank(uid)) {
return ResponseDto.failure(ErrorMapping.get("10001"));
}
if (param == null || StringUtils.isBlank(param.getBadgeId()) || StringUtils.isBlank(param.getProofImageUrl())) {
return ResponseDto.failure("参数错误");
}
String badgeId = StringUtils.trimToEmpty(param.getBadgeId());
String proofImageUrl = StringUtils.trimToEmpty(param.getProofImageUrl());
AdamCaomeiBadge badge = adamCaomeiBadgeMapper.selectOne(
Wrappers.lambdaQuery(AdamCaomeiBadge.class)
.eq(AdamCaomeiBadge::getBadgeId, badgeId)
.last("limit 1")
);
if (badge == null) {
return ResponseDto.failure(ErrorMapping.get("10607"));
}
if (badge.getType() == null || badge.getType() != 2) {
return ResponseDto.failure("仅演出纪念徽章支持补签申请");
}
int claimedCount = adamCaomeiBadgeMapper.checkUserBadgeExists(uid, badgeId);
if (claimedCount > 0) {
return ResponseDto.failure(ErrorMapping.get("10608"));
}
String performanceId = StringUtils.trimToEmpty(badge.getPerformanceId());
int pendingApply;
if (StringUtils.isNotBlank(performanceId)) {
pendingApply = countApplyForUserPerformanceAudit(uid, performanceId, 0);
} else {
Integer pendingCount = badgeApplyRecordMapper.selectCount(
Wrappers.lambdaQuery(AdamCaomeiBadgeApplyRecord.class)
.eq(AdamCaomeiBadgeApplyRecord::getUserId, uid)
.eq(AdamCaomeiBadgeApplyRecord::getBadgeId, badgeId)
.eq(AdamCaomeiBadgeApplyRecord::getAuditStatus, 0)
);
pendingApply = pendingCount == null ? 0 : pendingCount;
}
if (pendingApply > 0) {
return ResponseDto.failure("该场次已有待审核的补签申请,请勿重复提交");
}
final String applyRecordId = IDGenerator.nextSnowId();
long t = System.currentTimeMillis();
boolean autoPass = isPerformanceOngoing(performanceId);
if (autoPass) {
// 与 admin 审核通过保持一致:仅将申请记录标记为已通过,不自动发放徽章
queueUtils.sendMsgByRedis(
MQConst.AdamQueue.SQL_UCENTER.getKey(),
SqlMapping.get("adam_caomei_badge_apply_record.add_passed",
applyRecordId,
uid,
badgeId,
performanceId,
proofImageUrl)
);
} else {
queueUtils.sendMsgByRedis(
MQConst.AdamQueue.SQL_UCENTER.getKey(),
SqlMapping.get("adam_caomei_badge_apply_record.add",
applyRecordId,
uid,
badgeId,
performanceId,
proofImageUrl)
);
}
log.info("[claimBadge] MQ耗时:{}ms, uid: {}, badgeId: {}", System.currentTimeMillis() - t, uid, badge.getBadgeId());
return ResponseDto.success(applyRecordId);
}
/**
* 同场次 type=2 徽章 ID(用于补签记录 performance_id 为空等历史数据的 OR 查询)。
*/
private Set<String> type2BadgeIdsForPerformance(String performanceId) {
if (StringUtils.isBlank(performanceId)) {
return Collections.emptySet();
}
List<AdamCaomeiBadge> list = adamCaomeiBadgeMapper.selectList(
Wrappers.lambdaQuery(AdamCaomeiBadge.class)
.eq(AdamCaomeiBadge::getType, 2)
.eq(AdamCaomeiBadge::getPerformanceId, performanceId));
if (list == null || list.isEmpty()) {
return Collections.emptySet();
}
return list.stream()
.map(AdamCaomeiBadge::getBadgeId)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toSet());
}
/**
* 用户对某场次是否存在指定审核状态的补签记录(按 performance_id 或同场徽章 ID 命中)。
*/
private int countApplyForUserPerformanceAudit(String uid, String performanceId, int auditStatus) {
if (StringUtils.isBlank(performanceId)) {
return 0;
}
Set<String> samePerfBadgeIds = type2BadgeIdsForPerformance(performanceId);
LambdaQueryWrapper<AdamCaomeiBadgeApplyRecord> qw =
Wrappers.lambdaQuery(AdamCaomeiBadgeApplyRecord.class)
.eq(AdamCaomeiBadgeApplyRecord::getUserId, uid)
.eq(AdamCaomeiBadgeApplyRecord::getAuditStatus, auditStatus);
if (samePerfBadgeIds.isEmpty()) {
qw.eq(AdamCaomeiBadgeApplyRecord::getPerformanceId, performanceId);
} else {
qw.and(w -> w.eq(AdamCaomeiBadgeApplyRecord::getPerformanceId, performanceId)
.or()
.in(AdamCaomeiBadgeApplyRecord::getBadgeId, samePerfBadgeIds));
}
Integer n = badgeApplyRecordMapper.selectCount(qw);
return n == null ? 0 : n;
}
private boolean isPerformanceOngoing(String performanceId) {
if (StringUtils.isBlank(performanceId)) {
return false;
}
AdamRdmService.PerformanceTimeRange r = adamRdmService.getPerformanceTimeRangeById(performanceId);
if (r == null || r.getStart() == null || r.getEnd() == null) {
return false;
}
java.time.LocalDateTime now = java.time.LocalDateTime.now();
return (now.isEqual(r.getStart()) || now.isAfter(r.getStart()))
&& (now.isEqual(r.getEnd()) || now.isBefore(r.getEnd()));
}
}
package com.liquidnet.service.adam.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.liquidnet.commons.lang.util.CurrentUtil;
import com.liquidnet.commons.lang.util.DateUtil;
import com.liquidnet.service.adam.dto.AdamCaomeiPassportUserBadgeDto;
import com.liquidnet.service.adam.dto.vo.*;
import com.liquidnet.service.adam.entity.AdamCaomeiBadge;
import com.liquidnet.service.adam.entity.AdamCaomeiBadgeApplyRecord;
import com.liquidnet.service.adam.entity.AdamCaomeiPassport;
import com.liquidnet.service.adam.mapper.AdamCaomeiBadgeApplyRecordMapper;
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.adam.util.QueueUtils;
import com.liquidnet.service.base.ErrorMapping;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.base.SqlMapping;
import com.liquidnet.service.base.constant.MQConst;
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 AdamCaomeiBadgeApplyRecordMapper badgeApplyRecordMapper;
@Autowired
private AdamRdmService adamRdmService;
@Autowired
private QueueUtils queueUtils;
@Override
@Transactional(rollbackFor = Exception.class)
public ResponseDto<List<AdamCaomeiPassportUserClaimedBadgeVo>> 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),用于批量发放和返回给前端弹窗
List<AdamCaomeiBadge> publishedBadges = adamRdmService.getPublishedCaomeiBadges();
if (publishedBadges == null) {
publishedBadges = new ArrayList<>();
}
// 所有护照类型徽章
List<AdamCaomeiBadge> passportTypeBadges = publishedBadges.stream()
.filter(b -> b.getType() != null && b.getType() == 1)
.collect(Collectors.toList());
if (passportTypeBadges.isEmpty()) {
log.info("[bindPassport] 暂无上架徽章");
return ResponseDto.success();
}
List<String> badgeIds = passportTypeBadges.stream()
.map(AdamCaomeiBadge::getBadgeId)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toList());
Date grantAt = DateUtil.now();
// 兼容 claimBadge 的写法:把本次发放的护照徽章同时写入用户徽章缓存列表
List<AdamCaomeiPassportUserBadgeDto> cacheBadgeVos = adamRdmService.getUserCaomeiBadgesByUid(uid);
if (cacheBadgeVos == null) {
cacheBadgeVos = new ArrayList<>();
} else {
cacheBadgeVos = new ArrayList<>(cacheBadgeVos);
}
Set<String> existedBadgeIds = cacheBadgeVos.stream()
.map(AdamCaomeiPassportUserBadgeDto::getBadgeId)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toSet());
List<AdamCaomeiPassportUserBadgeDto> appendVos = passportTypeBadges.stream()
.filter(b -> b != null && StringUtils.isNotBlank(b.getBadgeId()) && !existedBadgeIds.contains(b.getBadgeId()))
.map(b -> {
AdamCaomeiPassportUserBadgeDto dto = new AdamCaomeiPassportUserBadgeDto();
dto.setBadgeId(b.getBadgeId());
dto.setBadgeName(StringUtils.defaultString(b.getName()));
dto.setIcon(StringUtils.defaultString(b.getIcon()));
dto.setShareText(StringUtils.defaultString(b.getShareText()));
dto.setType(b.getType());
dto.setClaimedAt(grantAt);
dto.setSource(1);
return dto;
})
.collect(Collectors.toList());
adamRdmService.addUserCaomeiBadgeDtosByUid(uid, cacheBadgeVos, appendVos);
LinkedList<Object[]> paramsList = new LinkedList<>();
for (String badgeId : badgeIds) {
paramsList.add(new Object[]{uid, badgeId, 1, grantAt});
}
queueUtils.sendMsgByRedis(
MQConst.AdamQueue.SQL_UCENTER.getKey(),
SqlMapping.get("adam_caomei_user_badge.add", paramsList)
);
// 5. 返回本次绑定场景下的护照类型徽章列表(用于前端弹窗)
List<AdamCaomeiPassportUserClaimedBadgeVo> grantedBadges = appendVos.stream().map(b -> {
AdamCaomeiPassportUserClaimedBadgeVo v = new AdamCaomeiPassportUserClaimedBadgeVo();
v.setBadgeId(b.getBadgeId());
v.setName(StringUtils.defaultString(b.getBadgeName()));
v.setIcon(StringUtils.defaultString(b.getIcon()));
v.setShareText(StringUtils.defaultString(b.getShareText()));
v.setType(b.getType());
v.setClaimedAt(b.getClaimedAt());
v.setSource(1);
return v;
}).collect(Collectors.toList());
log.info("[bindPassport] 护照绑定成功, uid: {}, passportSn: {}", uid, passportNo);
return ResponseDto.success(grantedBadges);
}
// 6. 如果原子性修改未成功,说明在并发情况下被别人抢先绑定了
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);
// 4. 获取用户当前绑定的护照信息
AdamCaomeiPassport bound = findBoundPassportForUser(uid);
if (null == bound) {
log.error("[getPassportHome] 用户未绑定护照, uid: {}", uid);
return ResponseDto.failure(ErrorMapping.get("10604")); // 未绑定护照
}
// 1. 初始化返回对象
AdamCaomeiPassportHomeVo home = new AdamCaomeiPassportHomeVo();
AdamCaomeiPassportUserCardVo card = buildUserCard(uid, bound);
home.setUserCard(card);
// 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));
// 5. TODO 优化点
final List<String> paidPerformanceIds = StringUtils.isNotBlank(idCard)
? adamRdmService.getPaidPerformanceIdsByIdCard(idCard)
: new ArrayList<>();
log.info("[getPassportHome] 用户已支付的演出订单数量, uid: {}, 数量: {}", uid, paidPerformanceIds.size());
// 先取上架徽章配置(供补签状态按演出聚合时做 badgeId→performanceId 兜底)
List<AdamCaomeiBadge> published = adamRdmService.getPublishedCaomeiBadges();
if (published == null) {
published = new ArrayList<>();
}
ApplyBadgeStatus applyBadgeStatus = loadApplyBadgeStatus(uid, published);
// 6. 查询用户已认领的所有徽章记录 (用于展示徽章墙)
List<AdamCaomeiPassportUserBadgeDto> rows = adamRdmService.getUserCaomeiBadgesByUid(uid);
if (rows == null) {
rows = new ArrayList<>();
}
Map<String, String> claimedPerformanceTitleById = buildClaimedPerformanceTitleMap(rows);
List<AdamCaomeiPassportUserClaimedBadgeVo> claimed = toClaimedBadgeVos(rows, claimedPerformanceTitleById);
home.setClaimedBadges(claimed);
log.info("[getPassportHome] 用户已认领的徽章数量, uid: {}, 数量: {}", uid, claimed.size());
// 转换为 Map 方便后续匹配货架上的徽章是否已认领
Map<String, AdamCaomeiPassportUserBadgeDto> claimedMap = toClaimedBadgeMap(rows);
log.info("[getPassportHome] 系统已上架的徽章数量, uid: {}, 数量: {}", uid, published.size());
// 演出纪念徽章:批量查演出名称,供前端按演出分组展示
Map<String, String> performanceTitleById = buildPerformanceTitleMap(published);
// 8. 组装全部上架徽章列表 (扁平结构,前端按 type / 演出名称 筛选分组展示)
List<AdamCaomeiPassportBadgeShelfItemVo> allBadges = toShelfItems(
published,
claimedMap,
paidPerformanceIds,
applyBadgeStatus,
performanceTitleById
);
home.setAllBadges(allBadges);
log.info("[getPassportHome] 获取护照首页数据成功, uid: {}", uid);
return ResponseDto.success(home);
}
@Override
public ResponseDto<Boolean> checkPassportBound() {
String uid = CurrentUtil.getCurrentUid();
AdamCaomeiPassport bound = findBoundPassportForUser(uid);
return ResponseDto.success(bound != null);
}
private AdamCaomeiPassportUserCardVo buildUserCard(String uid, AdamCaomeiPassport bound) {
AdamCaomeiPassportUserCardVo card = new AdamCaomeiPassportUserCardVo();
card.setPassportBound(true);
card.setPassportNo(bound.getPassportNo());
card.setPassportClaimedAt(bound.getBoundAt());
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("");
}
return card;
}
private ApplyBadgeStatus loadApplyBadgeStatus(String uid, List<AdamCaomeiBadge> published) {
Map<String, String> badgeIdToPerformanceId = new HashMap<>();
if (published != null) {
for (AdamCaomeiBadge b : published) {
if (b != null && StringUtils.isNotBlank(b.getBadgeId())) {
badgeIdToPerformanceId.put(b.getBadgeId(), StringUtils.trimToEmpty(b.getPerformanceId()));
}
}
}
List<AdamCaomeiBadgeApplyRecord> applyRecords = badgeApplyRecordMapper.selectList(
Wrappers.lambdaQuery(AdamCaomeiBadgeApplyRecord.class)
.eq(AdamCaomeiBadgeApplyRecord::getUserId, uid)
.in(AdamCaomeiBadgeApplyRecord::getAuditStatus, 0, 1)
);
Set<String> passedApplyBadgeIds = new HashSet<>();
Set<String> pendingApplyBadgeIds = new HashSet<>();
Set<String> passedApplyPerformanceIds = new HashSet<>();
Set<String> pendingApplyPerformanceIds = new HashSet<>();
for (AdamCaomeiBadgeApplyRecord r : applyRecords) {
if (r == null || StringUtils.isBlank(r.getBadgeId()) || r.getAuditStatus() == null) {
continue;
}
String perf = StringUtils.trimToEmpty(r.getPerformanceId());
if (StringUtils.isBlank(perf)) {
perf = StringUtils.trimToEmpty(badgeIdToPerformanceId.get(r.getBadgeId()));
}
if (r.getAuditStatus() == 1) {
passedApplyBadgeIds.add(r.getBadgeId());
if (StringUtils.isNotBlank(perf)) {
passedApplyPerformanceIds.add(perf);
}
} else if (r.getAuditStatus() == 0) {
pendingApplyBadgeIds.add(r.getBadgeId());
if (StringUtils.isNotBlank(perf)) {
pendingApplyPerformanceIds.add(perf);
}
}
}
return new ApplyBadgeStatus(
passedApplyBadgeIds,
pendingApplyBadgeIds,
passedApplyPerformanceIds,
pendingApplyPerformanceIds
);
}
private static List<AdamCaomeiPassportUserClaimedBadgeVo> toClaimedBadgeVos(List<AdamCaomeiPassportUserBadgeDto> rows,
Map<String, String> performanceTitleById) {
return rows.stream().map(r -> {
AdamCaomeiPassportUserClaimedBadgeVo v = new AdamCaomeiPassportUserClaimedBadgeVo();
v.setBadgeId(r.getBadgeId());
v.setName(StringUtils.defaultString(r.getBadgeName()));
v.setSubTitle(StringUtils.defaultString(r.getSubTitle()));
v.setIcon(StringUtils.defaultString(r.getIcon()));
v.setShareText(StringUtils.defaultString(r.getShareText()));
v.setType(r.getType());
if (r.getType() != null && r.getType() == 2 && StringUtils.isNotBlank(r.getPerformanceId())) {
String title = performanceTitleById != null ? performanceTitleById.get(r.getPerformanceId()) : null;
v.setPerformanceName(StringUtils.isNotBlank(title) ? title : r.getPerformanceId());
} else {
v.setPerformanceName("");
}
v.setClaimedAt(r.getClaimedAt());
v.setSource(r.getSource());
return v;
}).collect(Collectors.toList());
}
private Map<String, String> buildClaimedPerformanceTitleMap(List<AdamCaomeiPassportUserBadgeDto> claimedRows) {
List<String> perfIds = claimedRows.stream()
.filter(r -> r != null && r.getType() != null && r.getType() == 2)
.map(AdamCaomeiPassportUserBadgeDto::getPerformanceId)
.filter(StringUtils::isNotBlank)
.distinct()
.collect(Collectors.toList());
if (perfIds.isEmpty()) {
return Collections.emptyMap();
}
Map<String, String> map = new HashMap<>(perfIds.size() * 2);
for (String perfId : perfIds) {
String title = adamRdmService.getPerformanceTitleById(perfId);
if (StringUtils.isNotBlank(title)) {
map.put(perfId, title);
}
}
return map;
}
private static Map<String, AdamCaomeiPassportUserBadgeDto> toClaimedBadgeMap(List<AdamCaomeiPassportUserBadgeDto> rows) {
return rows.stream()
.filter(r -> StringUtils.isNotBlank(r.getBadgeId()))
.collect(Collectors.toMap(AdamCaomeiPassportUserBadgeDto::getBadgeId, Function.identity(), (a, b) -> a));
}
private Map<String, String> buildPerformanceTitleMap(List<AdamCaomeiBadge> published) {
List<String> perfIds = published.stream()
.filter(b -> b != null && b.getType() != null && b.getType() == 2)
.map(AdamCaomeiBadge::getPerformanceId)
.filter(StringUtils::isNotBlank)
.distinct()
.collect(Collectors.toList());
if (perfIds.isEmpty()) {
return Collections.emptyMap();
}
Map<String, String> map = new HashMap<>(perfIds.size() * 2);
for (String perfId : perfIds) {
String title = adamRdmService.getPerformanceTitleById(perfId);
if (StringUtils.isNotBlank(title)) {
map.put(perfId, title);
}
}
return map;
}
private static List<AdamCaomeiPassportBadgeShelfItemVo> toShelfItems(List<AdamCaomeiBadge> published,
Map<String, AdamCaomeiPassportUserBadgeDto> claimedMap,
List<String> paidPerformanceIds,
ApplyBadgeStatus applyBadgeStatus,
Map<String, String> performanceTitleById) {
return published.stream()
.map(b -> toShelfItem(b, claimedMap, paidPerformanceIds, applyBadgeStatus, performanceTitleById))
.collect(Collectors.toList());
}
/**
* @param b 徽章信息
* @param claimedMap 用户已领取的徽章
* @param paidPerformanceIds 当前账号实名身份证号码购买的演出IDs
* @param applyBadgeStatus 补签审核状态(按徽章 + 按同场次演出聚合)
* @param performanceTitleById 演出 ID → 演出名称(仅 type=2 使用)
* @return
*/
private static AdamCaomeiPassportBadgeShelfItemVo toShelfItem(AdamCaomeiBadge b,
Map<String, AdamCaomeiPassportUserBadgeDto> claimedMap,
List<String> paidPerformanceIds,
ApplyBadgeStatus applyBadgeStatus,
Map<String, String> performanceTitleById) {
AdamCaomeiPassportBadgeShelfItemVo v = new AdamCaomeiPassportBadgeShelfItemVo();
v.setBadgeId(b.getBadgeId());
v.setName(StringUtils.defaultString(b.getName()));
v.setSubTitle(StringUtils.defaultString(b.getSubTitle()));
v.setIcon(StringUtils.defaultString(b.getIcon()));
v.setShareText(StringUtils.defaultString(b.getShareText()));
v.setType(b.getType());
String perfId = StringUtils.defaultString(b.getPerformanceId());
v.setPerformanceId(perfId);
int type = b.getType() == null ? 0 : b.getType();
if (type == 2 && StringUtils.isNotBlank(perfId)) {
String title = performanceTitleById != null ? performanceTitleById.get(perfId) : null;
v.setPerformanceName(StringUtils.isNotBlank(title) ? title : perfId);
}
v.setApplyPending(false);
// 判断当前徽章是否已认领
AdamCaomeiPassportUserBadgeDto got = claimedMap.get(b.getBadgeId());
boolean claimed = got != null;
v.setClaimed(claimed);
v.setClaimedAt(got != null ? got.getClaimedAt() : null);
// 针对未认领的徽章,根据类型判断是否可认领 (claimable)
if (!claimed) {
if (type == 1) {
// 护照纪念徽章:只要绑定了护照,就可认领(通常是绑定时漏发或后来新上架的)
v.setClaimable(true);
} else if (type == 2) {
// 演出纪念徽章:有购票记录,或本场次任一补签审核通过,则本场次关联徽章均可认领
boolean canClaimByPaid = paidPerformanceIds != null && paidPerformanceIds.contains(b.getPerformanceId());
boolean canClaimByApplyThisBadge = applyBadgeStatus.getPassedApplyBadgeIds().contains(b.getBadgeId());
boolean canClaimByApplyThisPerf = StringUtils.isNotBlank(perfId)
&& applyBadgeStatus.getPassedApplyPerformanceIds().contains(perfId);
v.setClaimable(canClaimByPaid || canClaimByApplyThisBadge || canClaimByApplyThisPerf);
boolean pendingThisBadge = applyBadgeStatus.getPendingApplyBadgeIds().contains(b.getBadgeId());
boolean pendingThisPerf = StringUtils.isNotBlank(perfId)
&& applyBadgeStatus.getPendingApplyPerformanceIds().contains(perfId);
v.setApplyPending(pendingThisBadge || pendingThisPerf);
} 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")
);
}
private static final class ApplyBadgeStatus {
private final Set<String> passedApplyBadgeIds;
private final Set<String> pendingApplyBadgeIds;
private final Set<String> passedApplyPerformanceIds;
private final Set<String> pendingApplyPerformanceIds;
private ApplyBadgeStatus(Set<String> passedApplyBadgeIds,
Set<String> pendingApplyBadgeIds,
Set<String> passedApplyPerformanceIds,
Set<String> pendingApplyPerformanceIds) {
this.passedApplyBadgeIds = passedApplyBadgeIds;
this.pendingApplyBadgeIds = pendingApplyBadgeIds;
this.passedApplyPerformanceIds = passedApplyPerformanceIds;
this.pendingApplyPerformanceIds = pendingApplyPerformanceIds;
}
Set<String> getPassedApplyBadgeIds() {
return passedApplyBadgeIds;
}
Set<String> getPendingApplyBadgeIds() {
return pendingApplyBadgeIds;
}
Set<String> getPassedApplyPerformanceIds() {
return passedApplyPerformanceIds;
}
Set<String> getPendingApplyPerformanceIds() {
return pendingApplyPerformanceIds;
}
}
}
package com.liquidnet.service.adam.service.impl;
import com.liquidnet.common.exception.LiquidnetServiceException;
import com.liquidnet.commons.lang.util.CollectionUtil;
import com.liquidnet.commons.lang.util.CurrentUtil;
import com.liquidnet.commons.lang.util.IDGenerator;
......@@ -204,7 +205,21 @@ public class AdamUserServiceImpl implements IAdamUserService {
} else if (thirdPartVo.getOpenId().equals(param.getOpenId())) {
userInfoVo = adamRdmService.getUserInfoVoByUid(uid);
} else {
return ResponseDto.failure(ErrorMapping.get("10010"));
// login/tpa 带手机号:仅 QQ 允许同一 uid 下 openId 变更时无感知换绑;其它平台仍走原校验
if (!"QQ".equals(param.getPlatform())) {
return ResponseDto.failure(ErrorMapping.get("10010"));
}
String existUidForOpenId = adamRdmService.getUidByPlatformOpenId(param.getPlatform(), param.getOpenId());
if (StringUtils.isNotEmpty(existUidForOpenId) && !existUidForOpenId.equals(uid)) {
return ResponseDto.failure(ErrorMapping.get("10007"));
}
userInfoVo = adamRdmService.getUserInfoVoByUid(uid);
if (null == userInfoVo || userInfoVo.getState() == 2) {
log.warn("Cancelled mobile:{}", param.getMobile());
return ResponseDto.failure(ErrorMapping.get("10024"));
}
this.unBindTpa(uid, param.getPlatform());
this.bindTpa(uid, param);
}
}
return ResponseDto.success(userInfoVo);
......@@ -445,6 +460,15 @@ public class AdamUserServiceImpl implements IAdamUserService {
// adamRdmService.setCertification(1, idCard, name);
// }
// adamRdmService.identityHandler1(uid, name, idCard);
// 优先校验身份证是否已绑定其他账号(需要优先于三方三要素校验提示)
if (StringUtils.isNotBlank(idCard)) {
String boundUid = adamRdmService.getRealNameBoundUidByIdCard(idCard);
if (StringUtils.isNotBlank(boundUid) && !boundUid.equals(uid)) {
ErrorMapping.ErrorMessage errorMessage = ErrorMapping.get("10614");
throw new LiquidnetServiceException(errorMessage.getCode(), errorMessage.getMessage());
}
}
adamRdmService.identityHandler3(name, idCard, mobile);
AdamRealName realName = new AdamRealName();
......@@ -509,4 +533,32 @@ public class AdamUserServiceImpl implements IAdamUserService {
log.debug("#RDS耗时:{}ms", System.currentTimeMillis() - s);
return vo;
}
@Override
public AdamRealInfoVo verifyTwoElements(String uid, String name, String idCard) {
try {
adamRdmService.identityHandler1(uid, name, idCard);
AdamRealName realName = new AdamRealName();
realName.setRealNameId(IDGenerator.nextSnowId() + "");
realName.setUid(uid);
realName.setType(1);
realName.setNode(2);
realName.setName(name);
realName.setIdCard(idCard);
realName.setState(1);
realName.setCreatedAt(LocalDateTime.now());
adamRealNameService.add(realName);
AdamRealInfoVo vo = AdamRealInfoVo.getNew().copy(realName);
long s = System.currentTimeMillis();
adamRdmService.setRealInfoVoByUid(uid, vo);
log.debug("#RDS耗时:{}ms", System.currentTimeMillis() - s);
return vo;
}catch (Exception e) {
log.error("error", e);
return null;
}
}
}
......@@ -86,6 +86,23 @@
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=\u672A\u7ED1\u5B9A\u62A4\u7167
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
10607=\u5FBD\u7AE0\u4E0D\u5B58\u5728\u6216\u672A\u4E0A\u67B6
10608=\u60A8\u5DF2\u9886\u53D6\u8FC7\u8BE5\u5FBD\u7AE0
10609=\u8BA4\u9886\u62A4\u7167\u7EAA\u5FF5\u5FBD\u7AE0\u9700\u5148\u7ED1\u5B9A\u62A4\u7167
10610=\u8BA4\u9886\u6F14\u51FA\u5FBD\u7AE0\u9700\u5148\u5B9E\u540D
10611=\u60A8\u6682\u65E0\u8D2D\u7968\u8BB0\u5F55\uFF0C\u8BF7\u4E0A\u4F20\u8BA2\u5355\u622A\u56FE
10612=\u7279\u6B8A\u5FBD\u7AE0\u4E0D\u53EF\u81EA\u52A9\u9886\u53D6
10613=\u672A\u77E5\u7684\u5FBD\u7AE0\u7C7B\u578B
10614=\u8BE5\u5B9E\u540D\u4FE1\u606F\u5DF2\u7ED1\u5B9A\u5176\u4ED6\u8D26\u53F7
......@@ -80,10 +80,16 @@ adam_user_mobile_locate.update_province=UPDATE adam_user_mobile_locate SET provi
adam_user_mobile_locate.close=UPDATE adam_user_mobile_locate SET `state`=2, updated_at=? WHERE uid=? AND `state`=1
# ----------------------------------------------------
# \u8349\u8393\u62A4\u7167-\u7528\u6237\u5FBD\u7AE0\u83B7\u5F97\u8BB0\u5F55 (adam_caomei_user_badge)
# \u7528\u6237\u7AEF claimBadge\uFF1A\u4E0E AdamCaomeiBadgeUserServiceImpl \u4E2D\u300C\u5148\u5199 Redis \u7528\u6237\u5FBD\u7AE0\u5217\u8868\u3001\u518D MQ \u5F02\u6B65\u843D\u5E93\u300D\u914D\u5957\uFF1B\u53C2\u6570\u987A\u5E8F user_id, badge_id, source\uFF1Bcreated_at \u7531\u5E93\u7AEF now() \u5199\u5165
adam_caomei_user_badge.add=INSERT INTO adam_caomei_user_badge (user_id, badge_id, source, created_at) VALUES (?,?,?,?)
# \u8349\u8393\u62A4\u7167-\u5FBD\u7AE0\u8865\u7B7E\u7533\u8BF7 (adam_caomei_badge_apply_record)
# \u7528\u6237\u7AEF applyBadge\uFF1A\u4E0E\u300C\u5148\u5199 Redis \u8865\u7B7E\u5217\u8868\u3001\u518D MQ \u5F02\u6B65\u843D\u5E93\u300D\u914D\u5957\uFF1B\u53C2\u6570\u987A\u5E8F apply_record_id, user_id, badge_id, performance_id, proof_image_url\uFF1Baudit_status=0\u3001reject_reason \u7A7A\u4E32\u3001\u65F6\u95F4\u7531 now() \u5199\u5165
adam_caomei_badge_apply_record.add=INSERT INTO adam_caomei_badge_apply_record (apply_record_id, user_id, badge_id, performance_id, proof_image_url, audit_status, reject_reason, created_at, updated_at) VALUES (?,?,?,?,?,0,'',now(),now())
# ----------------------------------------------------
# 补签自动通过:直接写入 audit_status=1
adam_caomei_badge_apply_record.add_passed=INSERT INTO adam_caomei_badge_apply_record (apply_record_id, user_id, badge_id, performance_id, proof_image_url, audit_status, reject_reason, created_at, updated_at) VALUES (?,?,?,?,?,1,'',now(),now())
# ----------------------------------------------------
candy_user_coupon.close=UPDATE candy_user_coupon SET state=2,updated_at=sysdate(),operator='close' WHERE uid=? AND state=1
......
......@@ -64,6 +64,11 @@ public class GoblinFrontServiceImpl implements GoblinFrontService {
GoblinCouponService goblinCouponService;
@Autowired
private GoblinSqbPerformanceGoodsMapper goblinSqbPerformanceGoodsMapper;
private static final String SELECT_GOODS_EXCLUDE_NAME = "护照专属";
private boolean isPassportExclusive(GoblinGoodsInfoVo vo) {
return null != vo && StringUtil.isNotBlank(vo.getName()) && vo.getName().contains(SELECT_GOODS_EXCLUDE_NAME);
}
@Override
public ArrayList<GoblinFrontBannerVo> getListBanner() {
......@@ -806,7 +811,7 @@ public class GoblinFrontServiceImpl implements GoblinFrontService {
if (StringUtil.isNotBlank(spuids)) {
query.addCriteria(Criteria.where("spuId").nin(spuids.split(",")));
}
query.addCriteria(Criteria.where("delFlg").is("0").and("shelvesStatus").is("3").and("spuAppear").is("0").and("marketId").is(null).and("cateFid").nin("22196120924543", "22196122839313"));
query.addCriteria(Criteria.where("delFlg").is("0").and("shelvesStatus").is("3").and("spuAppear").is("0").and("marketId").is(null).and("cateFid").nin("22196120924543", "22196122839313").and("name").not().regex(Pattern.compile(SELECT_GOODS_EXCLUDE_NAME)));
//redis里面获取排序规则 1、上架时间2、销量3、价格高到低4、价格低到高
......@@ -924,7 +929,7 @@ public class GoblinFrontServiceImpl implements GoblinFrontService {
it.remove();
} else {
GoblinGoodsInfoVo goblinGoodsInfoVo = goblinRedisUtils.getGoodsInfoVo(goblinFrontSelectGoods.getSpuId());
if (null == goblinGoodsInfoVo || StringUtil.isNotBlank(goblinGoodsInfoVo.getMarketId())) {
if (null == goblinGoodsInfoVo || StringUtil.isNotBlank(goblinGoodsInfoVo.getMarketId()) || isPassportExclusive(goblinGoodsInfoVo)) {
it.remove();
}
}
......@@ -966,6 +971,9 @@ public class GoblinFrontServiceImpl implements GoblinFrontService {
if (isHidden(goblinGoodsInfoVo.getCateFid())) {
continue;
}
if (isPassportExclusive(goblinGoodsInfoVo)) {
continue;
}
goblinGoodsInfoVoArrayList.add(goblinGoodsInfoVo);
}
}
......@@ -1018,9 +1026,15 @@ public class GoblinFrontServiceImpl implements GoblinFrontService {
for (int i = 0; i < end; i++) {
if (i >= start) {
GoblinGoodsInfoVo goblinGoodsInfoVo = goblinRedisUtils.getGoodsInfoVo(spuidss[i]);
if (null == goblinGoodsInfoVo) {
continue;
}
if (isHidden(goblinGoodsInfoVo.getCateFid())) {
continue;
}
if (isPassportExclusive(goblinGoodsInfoVo)) {
continue;
}
goblinGoodsInfoVoArrayList.add(goblinGoodsInfoVo);
}
}
......
......@@ -366,6 +366,17 @@ public class KylinPerformancesServiceImpl implements IKylinPerformancesService {
}
}
}
if (ticketListNew.size() > 1) {
ticketListNew.sort((t1, t2) -> {
if (t1 == null || t1.getPrice() == null) {
return (t2 == null || t2.getPrice() == null) ? 0 : 1;
}
if (t2 == null || t2.getPrice() == null) {
return -1;
}
return t1.getPrice().compareTo(t2.getPrice());
});
}
partner.setTicketList(ticketListNew);
if (ticketListNew.size() == 0) {
ticketTimesList.remove(i);
......
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