package com.train.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.train.common.Result; import com.train.cfc.entity.CfcActivity; import com.train.cfc.mapper.CfcActivityMapper; import com.train.entity.TrainClass; import com.train.entity.TrainEnrollment; import com.train.entity.TrainGroupOrder; import com.train.entity.TrainGroupOrderMember; import com.train.mapper.TrainClassMapper; import com.train.mapper.TrainEnrollmentMapper; import com.train.mapper.TrainGroupOrderMapper; import com.train.mapper.TrainGroupOrderMemberMapper; import com.train.service.PayService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestAttribute; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ThreadLocalRandom; /** * 团报接口(FR-PAY-03):多人一单 + 企业开票信息采集。 *

流程:create 创建团报单(含成员名单与发票信息)→ pay 下单支付(test-mode 直付)→ 入账时逐人生成报名单。

*/ @Slf4j @Tag(name = "团报", description = "团报下单与支付") @RestController @RequestMapping("/api/group-order") public class GroupOrderController { @Resource private TrainGroupOrderMapper trainGroupOrderMapper; @Resource private TrainGroupOrderMemberMapper trainGroupOrderMemberMapper; @Resource private TrainClassMapper trainClassMapper; @Resource private TrainEnrollmentMapper trainEnrollmentMapper; @Resource private CfcActivityMapper cfcActivityMapper; @Resource private PayService payService; /** 占用名额的报名状态 */ private static final List OCCUPY_STATUS = Arrays.asList("pending", "paid", "confirmed"); /** * 创建团报单(含成员名单与开票信息),单价取 cfc activities 会员价。 */ @Operation(summary = "创建团报单") @PostMapping("/create") public Result> create(@RequestBody Map body, @RequestAttribute("userId") Long userId) { Object classIdObj = body.get("classId"); if (classIdObj == null) { return Result.error("请选择班次"); } Long classId = Long.valueOf(classIdObj.toString()); TrainClass trainClass = trainClassMapper.selectById(classId); if (trainClass == null || !"active".equals(trainClass.getStatus())) { return Result.error("班次不存在或不可报名"); } // 成员名单 Object membersObj = body.get("members"); if (!(membersObj instanceof List) || ((List) membersObj).isEmpty()) { return Result.error("请填写团报成员名单"); } List> memberList = new ArrayList<>(); @SuppressWarnings("unchecked") List rawMembers = (List) membersObj; for (Object m : rawMembers) { if (!(m instanceof Map)) { continue; } @SuppressWarnings("unchecked") Map mm = (Map) m; String name = mm.get("name") == null ? "" : mm.get("name").toString().trim(); String phone = mm.get("phone") == null ? "" : mm.get("phone").toString().trim(); if (!StringUtils.hasText(name) || !StringUtils.hasText(phone)) { return Result.error("成员姓名和手机号均不能为空"); } Map row = new HashMap<>(); row.put("name", name); row.put("phone", phone); memberList.add(row); } if (memberList.isEmpty()) { return Result.error("请填写团报成员名单"); } // 容量校验 Long occupied = trainEnrollmentMapper.selectCount( new LambdaQueryWrapper() .eq(TrainEnrollment::getClassId, classId) .in(TrainEnrollment::getStatus, OCCUPY_STATUS)); long newCount = memberList.size(); if (trainClass.getCapacity() != null && occupied != null && occupied + newCount > trainClass.getCapacity()) { return Result.error("该班次剩余名额不足,无法按当前人数团报"); } // 单价(会员价优先,与报名接口一致) int price = 0; if (trainClass.getActivityId() != null) { CfcActivity activity = cfcActivityMapper.selectById(trainClass.getActivityId()); if (activity != null) { int raw = activity.getPrice() == null ? 0 : activity.getPrice(); price = activity.getMemberPrice() == null ? raw : activity.getMemberPrice(); } } // 开票信息 String invoiceTitle = body.get("invoiceTitle") == null ? "" : body.get("invoiceTitle").toString().trim(); String invoiceType = body.get("invoiceType") == null ? "普票" : body.get("invoiceType").toString().trim(); if (StringUtils.hasText(invoiceTitle)) { if (!StringUtils.hasText(body.get("invoiceTaxNo") == null ? "" : body.get("invoiceTaxNo").toString())) { return Result.error("填写发票抬头后请一并填写税号"); } } TrainGroupOrder go = new TrainGroupOrder(); go.setGroupNo(genGroupNo()); go.setClassId(classId); go.setLeaderUid(userId); go.setMemberCount(memberList.size()); go.setUnitAmount(price); go.setTotalAmount(price * memberList.size()); go.setStatus("pending"); go.setInvoiceTitle(invoiceTitle); go.setInvoiceTaxNo(body.get("invoiceTaxNo") == null ? null : body.get("invoiceTaxNo").toString().trim()); go.setInvoiceEmail(body.get("invoiceEmail") == null ? null : body.get("invoiceEmail").toString().trim()); go.setInvoicePhone(body.get("invoicePhone") == null ? null : body.get("invoicePhone").toString().trim()); go.setInvoiceType(invoiceType); go.setCreatedAt(new Date()); go.setUpdatedAt(new Date()); trainGroupOrderMapper.insert(go); for (Map m : memberList) { TrainGroupOrderMember mem = new TrainGroupOrderMember(); mem.setGroupOrderId(go.getId()); mem.setName(m.get("name").toString()); mem.setPhone(m.get("phone").toString()); mem.setStatus("pending"); mem.setCreatedAt(new Date()); trainGroupOrderMemberMapper.insert(mem); } Map row = new HashMap<>(); row.put("id", go.getId()); row.put("groupNo", go.getGroupNo()); row.put("classId", classId); row.put("className", trainClass.getName()); row.put("memberCount", memberList.size()); row.put("unitAmount", price); row.put("totalAmount", go.getTotalAmount()); row.put("status", go.getStatus()); log.info("创建团报单: id={}, groupNo={}, classId={}, members={}, total={}", go.getId(), go.getGroupNo(), classId, memberList.size(), go.getTotalAmount()); return Result.success(row); } /** * 我的团报单列表(团长视角) */ @Operation(summary = "我的团报单列表") @PostMapping("/mine") public Result>> mine(@RequestAttribute("userId") Long userId) { List list = trainGroupOrderMapper.selectList( new LambdaQueryWrapper() .eq(TrainGroupOrder::getLeaderUid, userId) .orderByDesc(TrainGroupOrder::getId)); List> result = new ArrayList<>(); for (TrainGroupOrder go : list) { result.add(toRow(go)); } return Result.success(result); } /** * 团报单详情(含成员名单;须为团长本人) */ @Operation(summary = "团报单详情") @PostMapping("/detail") public Result> detail(@RequestBody Map body, @RequestAttribute("userId") Long userId) { Object idObj = body.get("id"); if (idObj == null) { return Result.error("缺少团报单ID"); } TrainGroupOrder go = trainGroupOrderMapper.selectById(Long.valueOf(idObj.toString())); if (go == null || !userId.equals(go.getLeaderUid())) { return Result.error("团报单不存在"); } Map row = toRow(go); List members = trainGroupOrderMemberMapper.selectList( new LambdaQueryWrapper() .eq(TrainGroupOrderMember::getGroupOrderId, go.getId()) .orderByAsc(TrainGroupOrderMember::getId)); List> memberRows = new ArrayList<>(); for (TrainGroupOrderMember m : members) { Map mm = new HashMap<>(); mm.put("id", m.getId()); mm.put("name", m.getName()); mm.put("phone", m.getPhone()); mm.put("status", m.getStatus()); mm.put("enrollmentId", m.getEnrollmentId()); memberRows.add(mm); } row.put("members", memberRows); return Result.success(row); } /** * 团报支付(test-mode 直接入账:订单置 paid + 逐成员生成报名单) */ @Operation(summary = "团报支付") @PostMapping("/pay") public Result> pay(@RequestBody Map body, @RequestAttribute("userId") Long userId) { Object idObj = body.get("id"); if (idObj == null) { return Result.error("缺少团报单ID"); } try { return Result.success(payService.createGroupOrderPay(Long.valueOf(idObj.toString()), userId)); } catch (RuntimeException e) { return Result.error(e.getMessage()); } } private Map toRow(TrainGroupOrder go) { Map row = new HashMap<>(); row.put("id", go.getId()); row.put("groupNo", go.getGroupNo()); row.put("classId", go.getClassId()); TrainClass c = go.getClassId() == null ? null : trainClassMapper.selectById(go.getClassId()); row.put("className", c != null ? c.getName() : null); row.put("leaderUid", go.getLeaderUid()); row.put("memberCount", go.getMemberCount()); row.put("unitAmount", go.getUnitAmount()); row.put("totalAmount", go.getTotalAmount()); row.put("orderId", go.getOrderId()); row.put("status", go.getStatus()); row.put("invoiceTitle", go.getInvoiceTitle()); row.put("invoiceType", go.getInvoiceType()); row.put("payTime", go.getPayTime()); row.put("createdAt", go.getCreatedAt()); return row; } /** 生成团报单号:GR + yyyyMMddHHmmss + 4位随机 */ private String genGroupNo() { String ts = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()); int rand = ThreadLocalRandom.current().nextInt(1000, 10000); return "GR" + ts + rand; } }