From ab7e92d5154b5acf05699bd527c4d8b5fff10550 Mon Sep 17 00:00:00 2001
From: zyy <zyy@email.com>
Date: Sat, 06 Dec 2025 19:01:30 +0800
Subject: [PATCH] 新增ATS

---
 trading-order-admin/src/main/java/com/yami/trading/api/controller/ats/ApiStockAtsController.java     |   71 +++++++
 trading-order-service/src/main/java/com/yami/trading/service/ats/impl/StockAtsServiceImpl.java       |  238 ++++++++++++++++++++++++++
 trading-order-service/src/main/java/com/yami/trading/dao/ats/StockAtsMapper.java                     |   18 ++
 trading-order-service/src/main/java/com/yami/trading/service/ats/StockAtsService.java                |   18 ++
 trading-order-admin/src/main/java/com/yami/trading/admin/controller/ats/AdminStockAtsController.java |   52 +++++
 trading-order-bean/src/main/java/com/yami/trading/bean/ats/StockAts.java                             |   56 ++++++
 trading-order-service/src/main/resources/mapper/ats/StockAtsMapper.xml                               |   22 ++
 trading-order-bean/src/main/java/com/yami/trading/bean/ats/dto/StockAtsDto.java                      |   17 +
 8 files changed, 492 insertions(+), 0 deletions(-)

diff --git a/trading-order-admin/src/main/java/com/yami/trading/admin/controller/ats/AdminStockAtsController.java b/trading-order-admin/src/main/java/com/yami/trading/admin/controller/ats/AdminStockAtsController.java
new file mode 100644
index 0000000..9d88c28
--- /dev/null
+++ b/trading-order-admin/src/main/java/com/yami/trading/admin/controller/ats/AdminStockAtsController.java
@@ -0,0 +1,52 @@
+package com.yami.trading.admin.controller.ats;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.yami.trading.bean.ats.dto.StockAtsDto;
+import com.yami.trading.common.domain.Result;
+import com.yami.trading.service.ats.StockAtsService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+
+@RestController
+@CrossOrigin
+@Api(tags = "后台股票ATS")
+@RequestMapping("stockAts")
+@Slf4j
+public class AdminStockAtsController {
+
+    @Resource
+    StockAtsService stockAtsService;
+
+    /**
+     * @Description: 获取ATS列表
+     * @Param:
+     * @return:
+     */
+    @ApiOperation("获取ATS订单")
+    @PostMapping({"getListByAdmin.do"})
+    public Result<Page<StockAtsDto>> getListByAdmin(@RequestParam(value = "pageNum", defaultValue = "1") int pageNum,
+                                                    @RequestParam(value = "pageSize", defaultValue = "5") int pageSize,
+                                                    @RequestParam(value = "keywords", required = false)String keywords) {
+        return stockAtsService.getListByAdmin(pageNum, pageSize, keywords);
+    }
+
+
+    @ApiOperation("ATS审核")
+    @PostMapping({"atsCheck.do"})
+    @ResponseBody
+    public Result atsCheck(@RequestParam(value = "id") String id,
+                                @RequestParam(value = "checkType") Integer checkType,
+                                @RequestParam(value = "stockCode", required = false) String stockCode,
+                                @RequestParam(value = "closePrice", defaultValue = "0", required = false) double closePrice,
+                                @RequestParam(value = "price",defaultValue = "0", required = false) double price) {
+        if((checkType == 1 && (id == null || stockCode == null) || (checkType == 2 && id == null))){
+            return Result.failed("参数不能为空");
+        }
+        return stockAtsService.atsCheck(id, checkType, stockCode, closePrice, price);
+    }
+
+}
diff --git a/trading-order-admin/src/main/java/com/yami/trading/api/controller/ats/ApiStockAtsController.java b/trading-order-admin/src/main/java/com/yami/trading/api/controller/ats/ApiStockAtsController.java
new file mode 100644
index 0000000..8966bc1
--- /dev/null
+++ b/trading-order-admin/src/main/java/com/yami/trading/api/controller/ats/ApiStockAtsController.java
@@ -0,0 +1,71 @@
+package com.yami.trading.api.controller.ats;
+
+import com.yami.trading.bean.dz.ExchangeApplyOrderDz;
+import com.yami.trading.common.constants.Constants;
+import com.yami.trading.common.domain.Result;
+import com.yami.trading.common.exception.YamiShopBindException;
+import com.yami.trading.security.common.util.SecurityUtils;
+import com.yami.trading.service.ats.StockAtsService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+@RestController
+@CrossOrigin
+@Api(tags = "股票Ats")
+@RequestMapping("api/stockAts")
+@Slf4j
+public class ApiStockAtsController {
+
+    @Resource
+    StockAtsService stockAtsService;
+
+    private static final ThreadLocal<Boolean> orderCreated = ThreadLocal.withInitial(() -> false);
+    private final Lock lock = new ReentrantLock();
+
+    @ApiOperation("ATS订单列表")
+    @PostMapping({"getOrderList.do"})
+    @ResponseBody
+    public Result getOrderList(@RequestParam(value = "pageNum", defaultValue = "1") int pageNum,
+                                 @RequestParam(value = "pageSize", defaultValue = "5") int pageSize) {
+        String partyId = SecurityUtils.getCurrentUserId();
+        if (partyId == null || partyId.isEmpty()) {
+            throw new YamiShopBindException("请先登录");
+        }
+        return stockAtsService.getList(pageNum, pageSize, partyId);
+    }
+
+    @ApiOperation("ATS下单")
+    @GetMapping({"buyStockAts.do"})
+    @ResponseBody
+    public Result buyDz(@RequestParam(name = "price") double price) {
+        lock.lock();
+        try {
+            if (orderCreated.get()) {
+                throw new YamiShopBindException("当前交易人数过多,请稍后重试");
+            }
+            orderCreated.set(true);
+            String partyId = SecurityUtils.getCurrentUserId();
+            if (partyId == null || partyId.isEmpty()) {
+                throw new YamiShopBindException("请先登录");
+            }
+            return stockAtsService.buyAts(price, partyId);
+        } catch (Exception e) {
+            log.error(e.getMessage());
+        }  finally{
+            lock.unlock();
+            orderCreated.set(false);
+        }
+        throw new YamiShopBindException("订单异常,请稍后重试");
+    }
+
+
+
+
+
+}
diff --git a/trading-order-bean/src/main/java/com/yami/trading/bean/ats/StockAts.java b/trading-order-bean/src/main/java/com/yami/trading/bean/ats/StockAts.java
new file mode 100644
index 0000000..2196d21
--- /dev/null
+++ b/trading-order-bean/src/main/java/com/yami/trading/bean/ats/StockAts.java
@@ -0,0 +1,56 @@
+package com.yami.trading.bean.ats;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.yami.trading.common.domain.UUIDEntity;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * ATS挂单
+ */
+@TableName("t_stock_ats")
+@Data
+public class StockAts extends UUIDEntity {
+
+    public final static String STATE_SUBMITTED = "submitted";
+    public final static String STATE_POSITION = "position";
+    public final static String STATE_FAILED = "failed";
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("股票名称")
+    private String stockName;
+
+    @ApiModelProperty("股票代码")
+    private String stockCode;
+
+    private String stockSpell;
+
+    private String userId;
+
+    @ApiModelProperty("订单号")
+    private String orderNo;
+
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date addTime;
+
+    @ApiModelProperty("购买金额")
+    private BigDecimal price;
+
+    @ApiModelProperty("买入价格")
+    private BigDecimal closePrice;
+
+    @ApiModelProperty("手续费")
+    private double fee;
+
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date buyTime;
+
+    @ApiModelProperty("状态.submitted 已提交,position 已转持仓,failed 失败")
+    private String state = "submitted";
+
+}
diff --git a/trading-order-bean/src/main/java/com/yami/trading/bean/ats/dto/StockAtsDto.java b/trading-order-bean/src/main/java/com/yami/trading/bean/ats/dto/StockAtsDto.java
new file mode 100644
index 0000000..0d84842
--- /dev/null
+++ b/trading-order-bean/src/main/java/com/yami/trading/bean/ats/dto/StockAtsDto.java
@@ -0,0 +1,17 @@
+package com.yami.trading.bean.ats.dto;
+
+import com.yami.trading.bean.ats.StockAts;
+import com.yami.trading.bean.dz.StockDz;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+public class StockAtsDto extends StockAts {
+
+    @ApiModelProperty("UID")
+    private String userCode;
+
+    @ApiModelProperty("用户名")
+    private String userName;
+
+}
diff --git a/trading-order-service/src/main/java/com/yami/trading/dao/ats/StockAtsMapper.java b/trading-order-service/src/main/java/com/yami/trading/dao/ats/StockAtsMapper.java
new file mode 100644
index 0000000..66716b7
--- /dev/null
+++ b/trading-order-service/src/main/java/com/yami/trading/dao/ats/StockAtsMapper.java
@@ -0,0 +1,18 @@
+package com.yami.trading.dao.ats;
+
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.yami.trading.bean.ats.StockAts;
+import com.yami.trading.bean.ats.dto.StockAtsDto;
+import org.apache.ibatis.annotations.Param;
+
+
+public interface StockAtsMapper extends BaseMapper<StockAts> {
+
+
+    Page<StockAtsDto> getListByAdmin(Page page,
+                                     @Param("keyWords") String keyWords);
+
+
+}
diff --git a/trading-order-service/src/main/java/com/yami/trading/service/ats/StockAtsService.java b/trading-order-service/src/main/java/com/yami/trading/service/ats/StockAtsService.java
new file mode 100644
index 0000000..017a3d9
--- /dev/null
+++ b/trading-order-service/src/main/java/com/yami/trading/service/ats/StockAtsService.java
@@ -0,0 +1,18 @@
+package com.yami.trading.service.ats;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.yami.trading.bean.ats.StockAts;
+import com.yami.trading.bean.ats.dto.StockAtsDto;
+import com.yami.trading.common.domain.Result;
+
+public interface StockAtsService extends IService<StockAts> {
+
+    Result getList(int pageNum, int pageSize, String partyId);
+
+    Result buyAts(double price, String partyId);
+
+    Result<Page<StockAtsDto>> getListByAdmin(int pageNum, int pageSize, String keywords);
+
+    Result atsCheck(String id, Integer checkType, String stockCode, double closePrice, double price);
+}
diff --git a/trading-order-service/src/main/java/com/yami/trading/service/ats/impl/StockAtsServiceImpl.java b/trading-order-service/src/main/java/com/yami/trading/service/ats/impl/StockAtsServiceImpl.java
new file mode 100644
index 0000000..5140ad2
--- /dev/null
+++ b/trading-order-service/src/main/java/com/yami/trading/service/ats/impl/StockAtsServiceImpl.java
@@ -0,0 +1,238 @@
+package com.yami.trading.service.ats.impl;
+
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.yami.trading.bean.ats.StockAts;
+import com.yami.trading.bean.ats.dto.StockAtsDto;
+import com.yami.trading.bean.contract.domain.ContractApplyOrder;
+import com.yami.trading.bean.dz.ExchangeApplyOrderDz;
+import com.yami.trading.bean.exchange.ExchangeApplyOrder;
+import com.yami.trading.bean.item.domain.Item;
+import com.yami.trading.bean.model.MoneyLog;
+import com.yami.trading.bean.model.User;
+import com.yami.trading.bean.model.Wallet;
+import com.yami.trading.bean.model.WalletExtend;
+import com.yami.trading.common.constants.Constants;
+import com.yami.trading.common.domain.Result;
+import com.yami.trading.common.exception.YamiShopBindException;
+import com.yami.trading.common.util.Arith;
+import com.yami.trading.common.util.RandomUtil;
+import com.yami.trading.dao.ats.StockAtsMapper;
+import com.yami.trading.service.MoneyLogService;
+import com.yami.trading.service.WalletService;
+import com.yami.trading.service.ats.StockAtsService;
+import com.yami.trading.service.exchange.ExchangeApplyOrderService;
+import com.yami.trading.service.item.ItemService;
+import com.yami.trading.service.user.UserService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Date;
+
+@Service
+@Slf4j
+public class StockAtsServiceImpl extends ServiceImpl<StockAtsMapper, StockAts> implements StockAtsService {
+
+    @Autowired
+    StockAtsMapper stockAtsMapper;
+    @Autowired
+    UserService userService;
+    @Autowired
+    WalletService walletService;
+    @Autowired
+    MoneyLogService moneyLogService;
+    @Autowired
+    ExchangeApplyOrderService exchangeApplyOrderService;
+    @Autowired
+    ItemService itemService;
+
+    @Override
+    public Result getList(int pageNum, int pageSize, String partyId) {
+        try {
+            QueryWrapper<StockAts> queryWrapper = new QueryWrapper<>();
+            queryWrapper.eq("user_id", partyId);
+
+            Page<StockAts> page = new Page<>(pageNum, pageSize);
+            page = stockAtsMapper.selectPage(page, queryWrapper);
+            return Result.succeed(page);
+        } catch (Exception e) {
+            log.error(e.getMessage());
+        }
+        return Result.failed("获取失败");
+    }
+
+    @Transactional
+    @Override
+    public Result buyAts(double price, String partyId) {
+        try {
+            if (price <= 0) {
+                throw new YamiShopBindException("请输入");
+            }
+            User party = userService.getById(partyId);
+            if (!party.isEnabled()) {
+                throw new YamiShopBindException("用户已禁用");
+            }
+
+            BigDecimal amt = BigDecimal.valueOf(price);
+
+            Wallet wallet = this.walletService.saveWalletByPartyId(partyId);
+            if (wallet.getMoney().compareTo(amt) < 0) {
+                return Result.failed("余额不足");
+            }
+
+
+            StockAts order = new StockAts();
+            order.setUserId(partyId);
+            order.setPrice(amt);
+            order.setAddTime(new Date());
+            order.setOrderNo(com.yami.trading.common.util.DateUtil.getToday("yyMMddHHmmss") + RandomUtil.getRandomNum(8));
+            stockAtsMapper.insert(order);
+
+            return Result.succeed("提交成功");
+        } catch (Exception e) {
+            log.error(e.getMessage());
+            return Result.failed("失败");
+        }
+    }
+
+    @Override
+    public Result<Page<StockAtsDto>> getListByAdmin(int pageNum, int pageSize, String keywords) {
+        try {
+            Page<StockAtsDto> page = new Page<>(pageNum, pageSize);
+            stockAtsMapper.getListByAdmin(page, keywords);
+            return Result.succeed(page);
+        } catch (Exception e) {
+            log.error(e.getMessage());
+        }
+        return Result.failed("获取失败");
+    }
+
+    @Transactional
+    @Override
+    public Result atsCheck(String id, Integer checkType, String stockCode, double closePrice, double price) {
+        try {
+            StockAts order = stockAtsMapper.selectById(id);
+            if (order == null) {
+                throw new YamiShopBindException("订单不存在");
+            }
+            if (!order.getState().equals(ExchangeApplyOrderDz.STATE_SUBMITTED)) {
+                throw new YamiShopBindException("订单已审核");
+            }
+            //1.通过  2.拒绝
+            if (checkType == 2) {
+                order.setState(ExchangeApplyOrderDz.STATE_FAILED);
+                stockAtsMapper.updateById(order);
+                return Result.succeed("操作成功");
+            }
+            if (price <= 0 || closePrice <= 0) {
+                throw new YamiShopBindException("输入正确金额");
+            }
+
+
+            Wallet wallet = this.walletService.saveWalletByPartyId(order.getUserId());
+            BigDecimal buyAmt = BigDecimal.valueOf(price);
+            BigDecimal closeAmt = BigDecimal.valueOf(closePrice);
+            BigDecimal orderFree = BigDecimal.ZERO;;
+            if (buyAmt.doubleValue() < 300) {
+                orderFree = BigDecimal.ONE;
+            }
+            BigDecimal orderAmt = buyAmt.add(orderFree);
+            if (wallet.getMoney().compareTo(orderAmt) < 0) {
+                throw new YamiShopBindException("订单失败,用户资金不足");
+            }
+
+            Item item = itemService.findBySymbol(stockCode);
+            if (item == null) {
+                throw new YamiShopBindException("股票代码不存在");
+            }
+            if (!item.getType().equalsIgnoreCase(Item.US_STOCKS)) {
+                throw new YamiShopBindException("请输入美股代码");
+            }
+
+            BigDecimal amountBefore = wallet.getMoney();
+
+            order.setStockCode(item.getSymbol());
+            order.setStockName(item.getName());
+            order.setStockSpell(item.getSymbolData());
+            order.setPrice(buyAmt);
+            order.setFee(orderFree.doubleValue());
+            order.setState(ExchangeApplyOrderDz.STATE_POSITION);
+            order.setBuyTime(new Date());
+            order.setClosePrice(closeAmt);
+            stockAtsMapper.updateById(order);
+            walletService.update(wallet.getUserId(), Arith.sub(0, orderAmt.doubleValue()));
+
+            //TODO 转持仓
+            intoPosition(order);
+
+            MoneyLog log = new MoneyLog();
+            log.setCategory(Constants.MONEYLOG_CATEGORY_EXCHANGE);
+            String name = Constants.MONEYLOG_MAP.get(Item.US_STOCKS);
+            String type2 = Constants.MONEYLOG_MAP_TYPE.get(Item.US_STOCKS);
+            log.setAmountBefore(amountBefore);
+            log.setAmount(orderAmt.negate());
+            log.setAmountAfter(amountBefore.subtract(orderAmt));
+            log.setLog(name + type2 + "ATS买入成交" + ",订单号[" + order.getOrderNo() + "]");
+            log.setUserId(order.getUserId());
+            log.setWalletType(Constants.WALLET);
+            log.setSymbol(order.getStockCode());
+            log.setContentType(type2 + Constants.MONEYLOG_CONTENT_OPEN);
+            log.setCreateTime(new Date());
+            log.setUpdateTime(new Date());
+            moneyLogService.save(log);
+            return Result.succeed("审核成功,订单已转持仓");
+        } catch (Exception e) {
+            log.error(e.getMessage());
+            return Result.failed("操作失败:" + e.getMessage());
+        }
+    }
+
+    public void intoPosition(StockAts order) {
+        //到账币种数量
+        BigDecimal buyNum = order.getPrice().divide(order.getClosePrice(), 5, RoundingMode.HALF_UP);
+
+        ExchangeApplyOrder exOrder = new ExchangeApplyOrder();
+        exOrder.setPartyId(order.getUserId());
+        exOrder.setSymbol(order.getStockCode());
+        exOrder.setOffset(ExchangeApplyOrder.OFFSET_OPEN);
+        exOrder.setSymbolValue(buyNum.doubleValue());
+        exOrder.setVolume(order.getPrice().doubleValue());
+        exOrder.setPrice(order.getClosePrice().doubleValue());
+        exOrder.setOrderPriceType(ContractApplyOrder.ORDER_PRICE_TYPE_OPPONENT);
+        exOrder.setState(ExchangeApplyOrder.STATE_CREATED);
+        exOrder.setClosePrice(order.getClosePrice().doubleValue());
+        exOrder.setOrderNo(com.yami.trading.common.util.DateUtil.getToday("yyMMddHHmmss") + RandomUtil.getRandomNum(8));
+        exOrder.setFee(order.getFee());
+        exOrder.setCreateTime(new Date());
+
+        double realValue = exOrder.getSymbolValue();
+        //入账
+        WalletExtend walletExtend = walletService.saveExtendByPara(exOrder.getPartyId(), exOrder.getSymbol());
+        double amountBeforeExtend = walletExtend.getAmount();
+        this.walletService.updateExtend(walletExtend.getPartyId(), walletExtend.getWallettype(), realValue);
+
+        MoneyLog log = new MoneyLog();
+        log.setCategory(Constants.MONEYLOG_CATEGORY_EXCHANGE);
+        String name = Constants.MONEYLOG_MAP.get(Item.US_STOCKS);
+        String type2 = Constants.MONEYLOG_MAP_TYPE.get(Item.US_STOCKS);
+        log.setAmountBefore(new BigDecimal(amountBeforeExtend));
+        log.setAmount(BigDecimal.valueOf(realValue));
+        log.setAmountAfter(BigDecimal.valueOf(amountBeforeExtend + realValue));
+        log.setLog(name + type2 + "ATS买入委托单成交,订单号[" + exOrder.getOrderNo() + "]");
+        log.setUserId(exOrder.getPartyId());
+        log.setSymbol(exOrder.getSymbol());
+        log.setWalletType(exOrder.getSymbol());
+        log.setContentType(type2 + Constants.MONEYLOG_CONTENT_OPEN);
+        // 记录账变日志
+        moneyLogService.save(log);
+
+        exchangeApplyOrderService.save(exOrder);
+    }
+
+}
diff --git a/trading-order-service/src/main/resources/mapper/ats/StockAtsMapper.xml b/trading-order-service/src/main/resources/mapper/ats/StockAtsMapper.xml
new file mode 100644
index 0000000..b8ad09a
--- /dev/null
+++ b/trading-order-service/src/main/resources/mapper/ats/StockAtsMapper.xml
@@ -0,0 +1,22 @@
+<?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.yami.trading.dao.ats.StockAtsMapper">
+
+
+    <select id="getListByAdmin" resultType="com.yami.trading.bean.ats.dto.StockAtsDto" parameterType="map">
+        SELECT
+        a.*,u.user_code,u.user_name
+        FROM t_stock_ats a
+        LEFT JOIN tz_user u ON u.user_id = a.user_id
+        WHERE 1=1
+        <if test="keyWords != null and keyWords != '' ">
+            AND (
+            u.user_name LIKE CONCAT('%', #{keyWords}, '%')
+            OR u.user_code LIKE CONCAT('%', #{keyWords}, '%')
+            )
+        </if>
+        ORDER BY  a.add_time DESC
+    </select>
+
+
+</mapper>

--
Gitblit v1.9.3