package com.talentcard.server.business.portal;
|
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
|
import com.talentcard.server.business.BusinessIdGenerator;
|
import com.talentcard.server.business.DatabaseRows;
|
import com.talentcard.server.common.BusinessException;
|
import com.talentcard.server.common.ErrorCode;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
@Service
|
public class CinemaPortalService {
|
private final JdbcTemplate jdbc;
|
public CinemaPortalService(JdbcTemplate jdbc) { this.jdbc = jdbc; }
|
|
@Transactional(readOnly = true)
|
public Map<String, Object> detail(Long merchantId) {
|
Map<String, Object> merchant = cinema(merchantId, false);
|
Map<String, Object> result = new LinkedHashMap<>();
|
result.put("merchantId", merchant.get("id"));
|
result.put("name", merchant.get("merchantName"));
|
result.put("heroImage", merchant.get("coverUrl"));
|
result.put("address", merchant.get("address"));
|
result.put("mobile", merchant.get("contactMobile"));
|
result.put("latitude", merchant.get("latitude"));
|
result.put("longitude", merchant.get("longitude"));
|
result.put("available", number(merchant.get("status")) == 1);
|
result.put("serviceLabel", text(merchant.get("serviceLabel"), "人才观影券"));
|
result.put("talentCardTag", "人才卡专享");
|
result.put("rightsTitle", "人才观影专属权益");
|
result.put("rightsDescription", text(merchant.get("rightsText"), "持人才卡可领取普通厅电影兑换券,到店出示券码后按影城规则兑换。具体适用影片、场次及补差金额以影城现场公示为准。"));
|
result.put("applicableCards", "卓越、杰出、菁英人才卡");
|
result.put("claimPrice", "0元");
|
result.put("usage", "到店出示券码");
|
result.put("scope", "普通厅指定场次");
|
return result;
|
}
|
|
@Transactional(readOnly = true)
|
public List<Map<String, Object>> coupons(Long merchantId, Long talentId) {
|
Map<String, Object> card = activeCard(talentId);
|
cinema(merchantId, false);
|
return query("""
|
select c.id as coupon_id, c.coupon_name, c.cover_url, c.claim_price, c.stock, c.per_user_limit,
|
c.valid_end, c.talent_levels, c.usage_text, c.scope_text, c.status,
|
exists(select 1 from cinema_coupon_claim cc where cc.coupon_id = c.id and cc.talent_id = ? and cc.deleted = 0) as claimed
|
from cinema_coupon c
|
where c.merchant_id = ? and c.deleted = 0 and c.status = 1
|
order by c.sort_no asc, c.id asc
|
""", talentId, merchantId).stream().map(coupon -> couponView(coupon, String.valueOf(card.get("cardLevel")))).toList();
|
}
|
|
@Transactional
|
public Map<String, Object> claim(Long merchantId, Long couponId, Long talentId, String idempotencyKey) {
|
if (idempotencyKey == null || idempotencyKey.isBlank() || idempotencyKey.length() > 128) {
|
throw new BusinessException(ErrorCode.BAD_REQUEST, "领取凭证不能为空");
|
}
|
List<Map<String, Object>> idempotent = query("select id from cinema_coupon_claim where idempotency_key = ? and deleted = 0", idempotencyKey);
|
if (!idempotent.isEmpty()) return claimView(Long.valueOf(number(idempotent.get(0).get("id"))));
|
|
Map<String, Object> card = activeCard(talentId);
|
Map<String, Object> merchant = cinema(merchantId, true);
|
List<Map<String, Object>> coupons = query("""
|
select id, merchant_id, coupon_name, stock, talent_levels, valid_end, status
|
from cinema_coupon where id = ? and merchant_id = ? and deleted = 0
|
""", couponId, merchantId);
|
if (coupons.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "电影券不存在");
|
Map<String, Object> coupon = coupons.get(0);
|
if (number(coupon.get("status")) != 1) throw new BusinessException(ErrorCode.CONFLICT, "电影券已失效");
|
if (!supportsCard(coupon.get("talentLevels"), String.valueOf(card.get("cardLevel")))) throw new BusinessException(ErrorCode.CONFLICT, "当前人才卡不适用该电影券");
|
List<Map<String, Object>> duplicate = query("select id from cinema_coupon_claim where coupon_id = ? and talent_id = ? and deleted = 0", couponId, talentId);
|
if (!duplicate.isEmpty()) throw new BusinessException(ErrorCode.CONFLICT, "优惠券已领取");
|
int updated = jdbc.update("update cinema_coupon set stock = stock - 1 where id = ? and merchant_id = ? and status = 1 and deleted = 0 and stock > 0", couponId, merchantId);
|
if (updated == 0) throw new BusinessException(ErrorCode.CONFLICT, "电影券已领完");
|
|
String claimNo = BusinessIdGenerator.next("MC");
|
String exchangeCode = BusinessIdGenerator.next("EX");
|
jdbc.update("""
|
insert into cinema_coupon_claim(claim_no, coupon_id, merchant_id, talent_id, card_id, card_no, valid_end, status, exchange_code, code_status, idempotency_key)
|
values (?, ?, ?, ?, ?, ?, ?, 'RECEIVED', ?, 'ACTIVE', ?)
|
""", claimNo, couponId, merchantId, talentId, card.get("cardId"), card.get("cardNo"), coupon.get("validEnd"), exchangeCode, idempotencyKey);
|
Long claimId = jdbc.queryForObject("select id from cinema_coupon_claim where idempotency_key = ?", Long.class, idempotencyKey);
|
jdbc.update("""
|
insert into movie_ticket_record(receive_no, package_name, merchants, talent_name, talent_mobile, talent_level, card_no, received_at)
|
values (?, ?, ?, ?, ?, ?, ?, now())
|
""", claimNo, coupon.get("couponName"), merchant.get("merchantName"), card.get("talentName"), card.get("talentMobile"), card.get("cardLevel"), card.get("cardNo"));
|
return claimView(claimId);
|
}
|
|
@Transactional(readOnly = true)
|
public Map<String, Object> claimDetail(Long claimId, Long talentId) {
|
if (claimId == null || talentId == null || talentId <= 0) throw new BusinessException(ErrorCode.BAD_REQUEST, "用户券参数不正确");
|
List<Map<String, Object>> claims = query("""
|
select cc.id as claim_id, cc.claim_no, cc.status as claim_status, cc.exchange_code, cc.code_status,
|
cc.valid_end, cc.used_at, c.coupon_name, c.annual_quota, c.usage_text, c.scope_text,
|
m.merchant_name
|
from cinema_coupon_claim cc
|
join cinema_coupon c on c.id = cc.coupon_id and c.deleted = 0
|
join merchant m on m.id = cc.merchant_id and m.deleted = 0
|
where cc.id = ? and cc.talent_id = ? and cc.deleted = 0
|
""", claimId, talentId);
|
if (claims.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "用户券不存在或无权查看");
|
Map<String, Object> claim = claims.get(0);
|
int annualTotal = number(claim.get("annualQuota"));
|
if (annualTotal < 1) annualTotal = 5;
|
Long annualClaimed = jdbc.queryForObject("""
|
select count(*) from cinema_coupon_claim
|
where talent_id = ? and deleted = 0 and year(created_at) = year(current_date)
|
""", Long.class, talentId);
|
int claimedCount = annualClaimed == null ? 0 : annualClaimed.intValue();
|
String codeStatus = effectiveCodeStatus(claim);
|
Map<String, Object> result = new LinkedHashMap<>();
|
result.put("claimId", claim.get("claimId"));
|
result.put("couponName", claim.get("couponName"));
|
result.put("merchantName", claim.get("merchantName"));
|
result.put("claimStatus", claim.get("claimStatus"));
|
result.put("annualTotal", annualTotal);
|
result.put("annualClaimed", claimedCount);
|
result.put("annualRemaining", Math.max(0, annualTotal - claimedCount));
|
result.put("exchangeCodeStatus", codeStatus);
|
result.put("exchangeCode", "ACTIVE".equals(codeStatus) ? claim.get("exchangeCode") : "");
|
result.put("validEnd", claim.get("validEnd"));
|
result.put("channel", "影城小程序");
|
result.put("verificationRule", text(claim.get("usageText"), "到店出示兑换码") + ",适用范围:" + text(claim.get("scopeText"), "以影城说明为准"));
|
result.put("invalidReason", codeStatusReason(codeStatus, claim.get("usedAt")));
|
return result;
|
}
|
|
private Map<String, Object> cinema(Long merchantId, boolean requireAvailable) {
|
List<Map<String, Object>> merchants = query("""
|
select id, merchant_name, merchant_type, contact_mobile, address, cover_url, service_label, rights_text,
|
latitude, longitude, status
|
from merchant where id = ? and deleted = 0 and merchant_type = 'cinema'
|
""", merchantId);
|
if (merchants.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "影城不存在");
|
Map<String, Object> merchant = merchants.get(0);
|
if (requireAvailable && number(merchant.get("status")) != 1) throw new BusinessException(ErrorCode.CONFLICT, "影城已下架");
|
return merchant;
|
}
|
|
private Map<String, Object> activeCard(Long talentId) {
|
if (talentId == null || talentId <= 0) throw new BusinessException(ErrorCode.BAD_REQUEST, "人才信息不能为空");
|
List<Map<String, Object>> cards = query("""
|
select c.id as card_id, c.card_no, c.card_level, u.name as talent_name, u.mobile as talent_mobile
|
from talent_card c join talent_user u on u.id = c.talent_id and u.deleted = 0
|
where c.talent_id = ? and c.deleted = 0 and c.card_type = 'PERSONAL' and c.status = 1
|
and (c.valid_end is null or c.valid_end >= current_date)
|
order by c.id desc limit 1
|
""", talentId);
|
if (cards.isEmpty()) throw new BusinessException(ErrorCode.CONFLICT, "当前用户未持有有效人才卡");
|
return cards.get(0);
|
}
|
|
private Map<String, Object> couponView(Map<String, Object> row, String cardLevel) {
|
Map<String, Object> result = new LinkedHashMap<>();
|
boolean claimed = Boolean.TRUE.equals(row.get("claimed")) || number(row.get("claimed")) == 1;
|
boolean supported = supportsCard(row.get("talentLevels"), cardLevel);
|
int stock = number(row.get("stock"));
|
result.put("couponId", row.get("couponId"));
|
result.put("name", row.get("couponName"));
|
result.put("coverUrl", row.get("coverUrl"));
|
result.put("claimPrice", row.get("claimPrice"));
|
result.put("stock", stock);
|
result.put("limitCount", row.get("perUserLimit"));
|
result.put("validEnd", row.get("validEnd"));
|
result.put("usage", row.get("usageText"));
|
result.put("scope", row.get("scopeText"));
|
result.put("claimed", claimed);
|
result.put("soldOut", stock < 1 || !supported);
|
result.put("unavailableReason", supported ? (stock < 1 ? "电影券已领完" : "") : "当前人才卡不适用该电影券");
|
return result;
|
}
|
|
private Map<String, Object> claimView(Long claimId) {
|
List<Map<String, Object>> claims = query("""
|
select cc.id as claim_id, cc.claim_no, cc.status, cc.valid_end, c.id as coupon_id, c.coupon_name, m.merchant_name
|
from cinema_coupon_claim cc
|
join cinema_coupon c on c.id = cc.coupon_id
|
join merchant m on m.id = cc.merchant_id
|
where cc.id = ? and cc.deleted = 0
|
""", claimId);
|
if (claims.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "领取记录不存在");
|
Map<String, Object> claim = claims.get(0);
|
Map<String, Object> result = new LinkedHashMap<>();
|
result.put("claimId", claim.get("claimId"));
|
result.put("couponId", claim.get("couponId"));
|
result.put("couponName", claim.get("couponName"));
|
result.put("merchantName", claim.get("merchantName"));
|
result.put("claimNo", claim.get("claimNo"));
|
result.put("status", claim.get("status"));
|
result.put("validEnd", claim.get("validEnd"));
|
result.put("dynamicCodeEntry", "/pages/coupon-received/coupon-received?claimId=" + claim.get("claimId"));
|
return result;
|
}
|
|
private String effectiveCodeStatus(Map<String, Object> claim) {
|
String status = String.valueOf(claim.getOrDefault("codeStatus", "ACTIVE"));
|
if ("USED".equals(status)) return "USED";
|
Object validEnd = claim.get("validEnd");
|
if (validEnd instanceof java.time.LocalDate date && date.isBefore(java.time.LocalDate.now())) return "EXPIRED";
|
return "ACTIVE";
|
}
|
private String codeStatusReason(String status, Object usedAt) {
|
if ("USED".equals(status)) return "兑换码已使用" + (usedAt == null ? "" : ",核销时间:" + usedAt);
|
if ("EXPIRED".equals(status)) return "兑换码已过期,请联系影城确认处理方式";
|
return "";
|
}
|
|
private boolean supportsCard(Object levels, String cardLevel) {
|
String configured = levels == null ? "" : String.valueOf(levels);
|
return configured.isBlank() || configured.contains(cardLevel);
|
}
|
private int number(Object value) { return value instanceof Number number ? number.intValue() : 0; }
|
private String text(Object value, String fallback) { return value == null || String.valueOf(value).isBlank() ? fallback : String.valueOf(value); }
|
private List<Map<String, Object>> query(String sql, Object... args) { return DatabaseRows.normalize(jdbc.queryForList(sql, args)); }
|
}
|