|
|
@@ -0,0 +1,269 @@
|
|
|
+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):多人一单 + 企业开票信息采集。
|
|
|
+ * <p>流程:create 创建团报单(含成员名单与发票信息)→ pay 下单支付(test-mode 直付)→ 入账时逐人生成报名单。</p>
|
|
|
+ */
|
|
|
+@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<String> OCCUPY_STATUS = Arrays.asList("pending", "paid", "confirmed");
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建团报单(含成员名单与开票信息),单价取 cfc activities 会员价。
|
|
|
+ */
|
|
|
+ @Operation(summary = "创建团报单")
|
|
|
+ @PostMapping("/create")
|
|
|
+ public Result<Map<String, Object>> create(@RequestBody Map<String, Object> 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<Map<String, Object>> memberList = new ArrayList<>();
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ List<Object> rawMembers = (List<Object>) membersObj;
|
|
|
+ for (Object m : rawMembers) {
|
|
|
+ if (!(m instanceof Map)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ Map<String, Object> mm = (Map<String, Object>) 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<String, Object> 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<TrainEnrollment>()
|
|
|
+ .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<String, Object> 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<String, Object> 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<List<Map<String, Object>>> mine(@RequestAttribute("userId") Long userId) {
|
|
|
+ List<TrainGroupOrder> list = trainGroupOrderMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<TrainGroupOrder>()
|
|
|
+ .eq(TrainGroupOrder::getLeaderUid, userId)
|
|
|
+ .orderByDesc(TrainGroupOrder::getId));
|
|
|
+ List<Map<String, Object>> result = new ArrayList<>();
|
|
|
+ for (TrainGroupOrder go : list) {
|
|
|
+ result.add(toRow(go));
|
|
|
+ }
|
|
|
+ return Result.success(result);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 团报单详情(含成员名单;须为团长本人)
|
|
|
+ */
|
|
|
+ @Operation(summary = "团报单详情")
|
|
|
+ @PostMapping("/detail")
|
|
|
+ public Result<Map<String, Object>> detail(@RequestBody Map<String, Object> 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<String, Object> row = toRow(go);
|
|
|
+ List<TrainGroupOrderMember> members = trainGroupOrderMemberMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<TrainGroupOrderMember>()
|
|
|
+ .eq(TrainGroupOrderMember::getGroupOrderId, go.getId())
|
|
|
+ .orderByAsc(TrainGroupOrderMember::getId));
|
|
|
+ List<Map<String, Object>> memberRows = new ArrayList<>();
|
|
|
+ for (TrainGroupOrderMember m : members) {
|
|
|
+ Map<String, Object> 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<Map<String, Object>> pay(@RequestBody Map<String, Object> 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<String, Object> toRow(TrainGroupOrder go) {
|
|
|
+ Map<String, Object> 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;
|
|
|
+ }
|
|
|
+}
|