From 64bc81d5f7bd99b470422b329aaca2182b79531c Mon Sep 17 00:00:00 2001
From: dd <gitluke@outlook.com>
Date: Mon, 01 Jun 2026 09:11:45 +0800
Subject: [PATCH] 1

---
 src/main/java/com/nq/service/impl/UserStockSubscribeServiceImpl.java |    2 
 src/main/java/com/nq/service/impl/StockServiceImpl.java              |   59 ++-
 src/main/java/com/nq/service/IPayService.java                        |    6 
 src/main/java/com/nq/controller/protol/UserController.java           |   14 +
 src/main/java/com/nq/service/impl/UserPositionServiceImpl.java       |  359 ++++++++++++++-----------
 src/main/java/com/nq/service/IUserPositionService.java               |    2 
 src/main/java/com/nq/service/IUserService.java                       |    3 
 src/main/java/com/nq/service/impl/UserServiceImpl.java               |   56 ++-
 src/main/resources/application.properties                            |   11 
 src/main/java/com/nq/utils/stock/sina/SinaStockApi.java              |  102 +++---
 src/main/java/com/nq/service/impl/SiteSettingServiceImpl.java        |    3 
 src/main/java/com/nq/service/impl/StockOptionServiceImpl.java        |   41 ++
 src/main/java/com/nq/controller/PayApiController.java                |   27 +
 src/main/java/com/nq/controller/protol/UserPayController.java        |    7 
 src/main/java/com/nq/service/impl/PayServiceImpl.java                |  136 +++++++++
 src/main/java/com/nq/service/impl/UserFundsPositionServiceImpl.java  |    2 
 16 files changed, 576 insertions(+), 254 deletions(-)

diff --git a/src/main/java/com/nq/controller/PayApiController.java b/src/main/java/com/nq/controller/PayApiController.java
index a572190..1c90d88 100644
--- a/src/main/java/com/nq/controller/PayApiController.java
+++ b/src/main/java/com/nq/controller/PayApiController.java
@@ -29,6 +29,8 @@
 
 import org.springframework.web.bind.annotation.RequestMapping;
 
+import org.springframework.web.bind.annotation.RequestMethod;
+
 import org.springframework.web.bind.annotation.ResponseBody;
 
 @Controller
@@ -162,4 +164,29 @@
             log.error("fly notify error Msg = {}", serverResponse.getMsg());
         }
     }
+
+    @RequestMapping(value = {"ococnReturn.do"}, method = {RequestMethod.GET, RequestMethod.POST})
+    public void ococnReturn(HttpServletRequest request, HttpServletResponse response) throws IOException {
+        String redirectUrl = this.iPayService.ococnReturn(request);
+        response.setContentType("text/html;charset=UTF-8");
+        response.setCharacterEncoding("UTF-8");
+        String safeUrl = redirectUrl.replace("\\", "\\\\").replace("'", "\\'");
+        response.getWriter().write(
+                "<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><title>跳转中</title>"
+                        + "<script>window.location.replace('" + safeUrl + "');</script>"
+                        + "</head><body></body></html>"
+        );
+    }
+
+    @RequestMapping(value = {"ococnNotify.do"}, method = {RequestMethod.GET, RequestMethod.POST})
+    @ResponseBody
+    public void ococnNotify(HttpServletRequest request, HttpServletResponse response) throws IOException {
+        ServerResponse serverResponse = this.iPayService.ococnNotify(request);
+        if (serverResponse.isSuccess()) {
+            response.getWriter().write("success");
+            log.info("ococn 支付渠道异步通知处理成功");
+        } else {
+            log.error("ococn notify error Msg = {}", serverResponse.getMsg());
+        }
+    }
 }
diff --git a/src/main/java/com/nq/controller/protol/UserController.java b/src/main/java/com/nq/controller/protol/UserController.java
index 2a74f6a..bfa1b4b 100644
--- a/src/main/java/com/nq/controller/protol/UserController.java
+++ b/src/main/java/com/nq/controller/protol/UserController.java
@@ -125,6 +125,20 @@
         }
         return serverResponse;
     }
+    //撤销委托单
+    @RequestMapping({"cancelOrder.do"})
+    @ResponseBody
+    public ServerResponse cancelOrder(HttpServletRequest request, @RequestParam("positionSn") String positionSn) {
+        ServerResponse serverResponse = null;
+        try {
+            serverResponse = this.iUserPositionService.cancelOrder(positionSn, request);
+        } catch (Exception e) {
+            log.error("用户撤单操作 = {}", e);
+            serverResponse = ServerResponse.createByErrorMsg("撤单失败");
+        }
+        return serverResponse;
+    }
+
     //用户平仓操作
     @RequestMapping({"sell.do"})
     @ResponseBody
diff --git a/src/main/java/com/nq/controller/protol/UserPayController.java b/src/main/java/com/nq/controller/protol/UserPayController.java
index 6356f8c..fcdfac8 100644
--- a/src/main/java/com/nq/controller/protol/UserPayController.java
+++ b/src/main/java/com/nq/controller/protol/UserPayController.java
@@ -49,4 +49,11 @@
         return this.iPayService.flyPay(payType, payAmt, currency, request);
     }
 
+    @RequestMapping({"ococnPay.do"})
+    @ResponseBody
+    public ServerResponse ococnPay(@RequestParam("payType") String payType, @RequestParam("payAmt") String payAmt, HttpServletRequest request) {
+        log.info("发起 ococn 线上支付 payType = {} payAmt = {}", payType, payAmt);
+        return this.iPayService.ococnPay(payType, payAmt, request);
+    }
+
 }
diff --git a/src/main/java/com/nq/service/IPayService.java b/src/main/java/com/nq/service/IPayService.java
index 2179583..0118f77 100644
--- a/src/main/java/com/nq/service/IPayService.java
+++ b/src/main/java/com/nq/service/IPayService.java
@@ -20,4 +20,10 @@
   ServerResponse flyPay(String paramString1, String paramString2, String paramString3, HttpServletRequest paramHttpServletRequest);
   
   ServerResponse flyNotify(HttpServletRequest paramHttpServletRequest);
+
+  ServerResponse ococnPay(String payType, String payAmt, HttpServletRequest request);
+
+  ServerResponse ococnNotify(HttpServletRequest request);
+
+  String ococnReturn(HttpServletRequest request);
 }
diff --git a/src/main/java/com/nq/service/IUserPositionService.java b/src/main/java/com/nq/service/IUserPositionService.java
index 9f3da22..ee57f2b 100644
--- a/src/main/java/com/nq/service/IUserPositionService.java
+++ b/src/main/java/com/nq/service/IUserPositionService.java
@@ -16,6 +16,8 @@
 
   ServerResponse pending(Integer paramInteger1, Integer paramInteger2, Integer paramInteger3, Integer paramInteger4,BigDecimal paramInteger5,BigDecimal paramInteger6, HttpServletRequest paramHttpServletRequest) throws Exception;
 
+  ServerResponse cancelOrder(String positionSn, HttpServletRequest request) throws Exception;
+
   ServerResponse sell(String paramString, int paramInt) throws Exception;
 
   ServerResponse calendar(String paramString,HttpServletRequest request) throws Exception;
diff --git a/src/main/java/com/nq/service/IUserService.java b/src/main/java/com/nq/service/IUserService.java
index 0518fd9..f282c63 100644
--- a/src/main/java/com/nq/service/IUserService.java
+++ b/src/main/java/com/nq/service/IUserService.java
@@ -92,6 +92,7 @@
 
   void updateUserAmt(Double amt, Integer user_id);
 
-
+  /** 将数据库中最新的用户资金写回登录缓存 */
+  void syncUserCache(HttpServletRequest request);
 
 }
diff --git a/src/main/java/com/nq/service/impl/PayServiceImpl.java b/src/main/java/com/nq/service/impl/PayServiceImpl.java
index 20d7e71..9372ad2 100644
--- a/src/main/java/com/nq/service/impl/PayServiceImpl.java
+++ b/src/main/java/com/nq/service/impl/PayServiceImpl.java
@@ -12,8 +12,10 @@
 import com.nq.utils.PropertiesUtil;
 import com.nq.utils.pay.CmcPayOuterRequestUtil;
 import com.nq.utils.pay.CmcPayTool;
+import com.nq.utils.pay.OcocnPayUtil;
 import com.nq.vo.pay.FlyPayVO;
 import com.nq.vo.pay.GuoPayVO;
+import com.nq.vo.pay.OcocnPayVO;
 
 import java.io.UnsupportedEncodingException;
 import java.math.BigDecimal;
@@ -22,6 +24,7 @@
 import java.security.NoSuchAlgorithmException;
 import java.text.SimpleDateFormat;
 import java.util.Date;
+import java.util.Map;
 import java.util.Random;
 import javax.servlet.http.HttpServletRequest;
 
@@ -375,6 +378,139 @@
     }
 
     @Override
+    public ServerResponse ococnPay(String payType, String payAmt, HttpServletRequest request) {
+        if (StringUtils.isBlank(payType) || StringUtils.isBlank(payAmt)) {
+            return ServerResponse.createByErrorMsg("参数不能为空");
+        }
+        BigDecimal payAmtBig = new BigDecimal(payAmt);
+        if (payAmtBig.compareTo(BigDecimal.ZERO) <= 0) {
+            return ServerResponse.createByErrorMsg("支付金额必须大于0");
+        }
+
+        User user = this.iUserService.getCurrentRefreshUser(request);
+        if (user == null) {
+            return ServerResponse.createByErrorMsg("请先登录");
+        }
+
+        String ordersn = KeyUtils.getRechargeOrderSn();
+        String money = payAmtBig.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString();
+        String pid = PropertiesUtil.getProperty("ococn.pay.pid");
+        String key = PropertiesUtil.getProperty("ococn.pay.key");
+        String submitUrl = PropertiesUtil.getProperty("ococn.pay.url");
+        String apiDomain = trimTrailingSlash(PropertiesUtil.getProperty("website.domain.url"));
+        if (StringUtils.isAnyBlank(pid, key, submitUrl, apiDomain)) {
+            return ServerResponse.createByErrorMsg("支付配置不完整,请联系管理员");
+        }
+        String notifyUrl = apiDomain + "/api/pay/ococnNotify.do";
+        String returnUrl = apiDomain + "/api/pay/ococnReturn.do";
+        String sitename = StringUtils.defaultString(PropertiesUtil.getProperty("ococn.pay.sitename", ""), "");
+        String productName = PropertiesUtil.getProperty("ococn.pay.name", "账户充值");
+
+        UserRecharge userRecharge = new UserRecharge();
+        userRecharge.setUserId(user.getId());
+        userRecharge.setNickName(user.getRealName());
+        userRecharge.setAgentId(user.getAgentId());
+        userRecharge.setOrderSn(ordersn);
+        userRecharge.setPayChannel(getOcocnChannelName(payType));
+        userRecharge.setPayAmt(payAmtBig);
+        userRecharge.setOrderStatus(Integer.valueOf(0));
+        userRecharge.setAddTime(new Date());
+
+        int insertCount = this.userRechargeMapper.insert(userRecharge);
+        if (insertCount <= 0) {
+            return ServerResponse.createByErrorMsg("创建支付订单失败");
+        }
+
+        String sign = OcocnPayUtil.buildSubmitSign(money, productName, notifyUrl, ordersn, pid, returnUrl, sitename, payType, key);
+        log.info("ococn支付 notifyUrl={} returnUrl={}", notifyUrl, returnUrl);
+        StringBuilder payUrlBuilder = new StringBuilder(submitUrl).append("?");
+        payUrlBuilder.append("pid=").append(pid)
+                .append("&type=").append(payType)
+                .append("&out_trade_no=").append(ordersn)
+                .append("&notify_url=").append(OcocnPayUtil.encode(notifyUrl))
+                .append("&return_url=").append(OcocnPayUtil.encode(returnUrl))
+                .append("&name=").append(OcocnPayUtil.encode(productName))
+                .append("&money=").append(money);
+        if (StringUtils.isNotBlank(sitename)) {
+            payUrlBuilder.append("&sitename=").append(OcocnPayUtil.encode(sitename));
+        }
+        payUrlBuilder.append("&sign=").append(sign).append("&sign_type=MD5");
+        String payUrl = payUrlBuilder.toString();
+
+        OcocnPayVO ococnPayVO = new OcocnPayVO();
+        ococnPayVO.setPayUrl(payUrl);
+        log.info("ococn支付,创建订单成功 orderSn={}", ordersn);
+        return ServerResponse.createBySuccess(ococnPayVO);
+    }
+
+    @Override
+    public ServerResponse ococnNotify(HttpServletRequest request) {
+        Map<String, String> params = OcocnPayUtil.parseRequestParams(request);
+        log.info("ococn支付通知参数: {}", params);
+
+        String outTradeNo = params.get("out_trade_no");
+        String tradeNo = params.get("trade_no");
+        String money = params.get("money");
+        String tradeStatus = params.get("trade_status");
+        String sign = params.get("sign");
+
+        if (StringUtils.isAnyBlank(outTradeNo, tradeNo, money, tradeStatus, sign)) {
+            return ServerResponse.createByErrorMsg("回调参数不完整");
+        }
+
+        String key = PropertiesUtil.getProperty("ococn.pay.key");
+        if (!OcocnPayUtil.verifyNotifySign(params, key)) {
+            log.error("ococn支付通知签名校验失败, remoteSign={}, params={}", sign, params);
+            return ServerResponse.createByErrorMsg("签名校验失败");
+        }
+
+        if (!"TRADE_SUCCESS".equals(tradeStatus)) {
+            return ServerResponse.createByErrorMsg("支付未成功");
+        }
+
+        UserRecharge existing = this.userRechargeMapper.findUserRechargeByOrderSn(outTradeNo);
+        if (existing != null && existing.getOrderStatus().intValue() != 0) {
+            return ServerResponse.createBySuccessMsg("订单已处理");
+        }
+
+        return doSuccess(outTradeNo, money);
+    }
+
+    @Override
+    public String ococnReturn(HttpServletRequest request) {
+        Map<String, String> params = OcocnPayUtil.parseRequestParams(request);
+        log.info("ococn支付同步跳转: {}", params);
+        String frontendUrl = StringUtils.defaultIfBlank(
+                PropertiesUtil.getProperty("ococn.pay.frontend_redirect"),
+                trimTrailingSlash(PropertiesUtil.getProperty("frontend.domain.url", "")) + "/#/user"
+        );
+        return frontendUrl;
+    }
+
+    private String getOcocnChannelName(String payType) {
+        if ("alipay".equals(payType)) {
+            return "支付宝-线上";
+        }
+        if ("wxpay".equals(payType)) {
+            return "微信-线上";
+        }
+        if ("qqpay".equals(payType)) {
+            return "QQ钱包-线上";
+        }
+        if ("tenpay".equals(payType)) {
+            return "财付通-线上";
+        }
+        return "线上支付";
+    }
+
+    private String trimTrailingSlash(String url) {
+        if (url == null) {
+            return null;
+        }
+        return url.endsWith("/") ? url.substring(0, url.length() - 1) : url;
+    }
+
+    @Override
     public ServerResponse juhenewpayNotify(HttpServletRequest request) throws UnsupportedEncodingException {
         LinkedMap map = new LinkedMap();
         String out_trade_no = request.getParameter("out_trade_no");
diff --git a/src/main/java/com/nq/service/impl/SiteSettingServiceImpl.java b/src/main/java/com/nq/service/impl/SiteSettingServiceImpl.java
index 78c5299..bc165ba 100644
--- a/src/main/java/com/nq/service/impl/SiteSettingServiceImpl.java
+++ b/src/main/java/com/nq/service/impl/SiteSettingServiceImpl.java
@@ -9,6 +9,7 @@
 import com.nq.pojo.SiteSetting;
 
 import com.nq.service.ISiteSettingService;
+import com.nq.utils.TradeFeeUtil;
 
 import java.util.List;
 
@@ -34,6 +35,7 @@
         if (list.size() > 0) {
 
             siteSetting = (SiteSetting) list.get(0);
+            siteSetting.setBuyFee(TradeFeeUtil.BUY_FEE_RATE);
 
         }
         return siteSetting;
@@ -48,6 +50,7 @@
         if (siteSetting == null) {
             return ServerResponse.createByErrorMsg("查不到设置记录");
         }
+        setting.setBuyFee(TradeFeeUtil.BUY_FEE_RATE);
 
         int updateCount = this.siteSettingMapper.updateByPrimaryKeySelective(setting);
 
diff --git a/src/main/java/com/nq/service/impl/StockOptionServiceImpl.java b/src/main/java/com/nq/service/impl/StockOptionServiceImpl.java
index 51c50b7..787fdce 100644
--- a/src/main/java/com/nq/service/impl/StockOptionServiceImpl.java
+++ b/src/main/java/com/nq/service/impl/StockOptionServiceImpl.java
@@ -71,9 +71,14 @@
 
      List<StockOptionListVO> stockOptionListVOS = Lists.newArrayList();
      for (StockOption option : stockOptions) {
-       StockOptionListVO stockOptionListVO = assembleStockOptionListVO(option);
-       stockOptionListVO.setIsOption("1");
-       stockOptionListVOS.add(stockOptionListVO);
+       try {
+         StockOptionListVO stockOptionListVO = assembleStockOptionListVO(option);
+         stockOptionListVO.setIsOption("1");
+         stockOptionListVOS.add(stockOptionListVO);
+       } catch (Exception e) {
+         log.error("自选列表单条行情组装失败, gid={}, code={}", option.getStockGid(), option.getStockCode(), e);
+         stockOptionListVOS.add(buildFallbackOptionVO(option));
+       }
      }
      PageInfo pageInfo = new PageInfo(stockOptions);
 
@@ -133,24 +138,42 @@
                  stockVO = SinaStockApi.assembleStockVO(SinaStockApi.getSinaStock(option.getStockGid()));
              }
          }
-         stockOptionListVO.setNowPrice(stockVO.getNowPrice());
+         if (stockVO == null) {
+             stockVO = new StockVO();
+         }
+         stockOptionListVO.setNowPrice(stockVO.getNowPrice() == null ? "0" : stockVO.getNowPrice());
 
-         stockOptionListVO.setHcrate(stockVO.getHcrate().toString());
+         stockOptionListVO.setHcrate(stockVO.getHcrate() == null ? "0" : stockVO.getHcrate().toString());
 
-         stockOptionListVO.setPreclose_px(stockVO.getPreclose_px());
+         stockOptionListVO.setPreclose_px(stockVO.getPreclose_px() == null ? "0" : stockVO.getPreclose_px());
 
-         stockOptionListVO.setOpen_px(stockVO.getOpen_px());
+         stockOptionListVO.setOpen_px(stockVO.getOpen_px() == null ? "0" : stockVO.getOpen_px());
 
          stockOptionListVO.setType(stockVO.getType());
 
          Stock stock = this.stockMapper.selectByPrimaryKey(option.getStockId());
 
-       stockOptionListVO.setStock_plate(stock.getStockPlate()==null?"":stock.getStockPlate());
-
+       if (stock != null) {
+         stockOptionListVO.setStock_plate(stock.getStockPlate()==null?"":stock.getStockPlate());
          stockOptionListVO.setStock_type(stock.getStockType());
+       }
 
 
          return stockOptionListVO;
 
      }
+
+   private StockOptionListVO buildFallbackOptionVO(StockOption option) {
+     StockOptionListVO vo = new StockOptionListVO();
+     vo.setId(option.getId().intValue());
+     vo.setStockName(option.getStockName());
+     vo.setStockCode(option.getStockCode());
+     vo.setStockGid(option.getStockGid());
+     vo.setNowPrice("0");
+     vo.setHcrate("0");
+     vo.setPreclose_px("0");
+     vo.setOpen_px("0");
+     vo.setIsOption("1");
+     return vo;
+   }
  }
diff --git a/src/main/java/com/nq/service/impl/StockServiceImpl.java b/src/main/java/com/nq/service/impl/StockServiceImpl.java
index a4d61ea..561000c 100644
--- a/src/main/java/com/nq/service/impl/StockServiceImpl.java
+++ b/src/main/java/com/nq/service/impl/StockServiceImpl.java
@@ -116,34 +116,49 @@
     PageHelper.startPage(pageNum, pageSize);
     User user = iUserService.getCurrentUser(request);
     List<Stock> stockList = this.stockMapper.findStockListByKeyWords(keyWords, stockPlate, stockType, Integer.valueOf(0));
+    boolean quickSearch = org.apache.commons.lang3.StringUtils.isNotBlank(keyWords);
 
     List<StockListVO> stockListVOS = Lists.newArrayList();
-    if (stockList.size() > 0)
-      for (Stock stock : stockList) {
-        StockListVO stockListVO = new StockListVO();
-        stockListVO=SinaStockApi.assembleLideStockListVO(LiDeDataUtils.getStock(stock.getStockCode()));
-        if(ObjectUtils.isEmpty(stockListVO)){
-          stockListVO = SinaStockApi.assembleStockListVO(SinaStockApi.getSinaStock(stock.getStockGid()));
-        }
-        stockListVO.setCode(stock.getStockCode());
-        stockListVO.setSpell(stock.getStockSpell());
-        stockListVO.setGid(stock.getStockGid());
-        BigDecimal day3Rate = (BigDecimal)selectRateByDaysAndStockCode(stock.getStockCode(), 3).getData();
-        stockListVO.setDay3Rate(day3Rate);
-        stockListVO.setStock_plate(stock.getStockPlate());
-        stockListVO.setStock_type(stock.getStockType());
-        //是否添加自选
-        if(user == null){
-          stockListVO.setIsOption("0");
-        } else {
-          stockListVO.setIsOption(iStockOptionService.isMyOption(user.getId(), stock.getStockCode()));
-        }
-        stockListVOS.add(stockListVO);
-      }
+    if (stockList.size() > 0) {
+      Integer userId = user != null ? user.getId() : null;
+      stockListVOS = stockList.parallelStream().map(stock -> assembleStockListItem(stock, userId, quickSearch))
+              .collect(java.util.stream.Collectors.toList());
+    }
     PageInfo pageInfo = new PageInfo(stockList);
     pageInfo.setList(stockListVOS);
     return ServerResponse.createBySuccess(pageInfo);
   }
+
+  /** 搜索列表组装行情:关键词搜索走轻量路径,跳过三日涨幅等慢查询 */
+  private StockListVO assembleStockListItem(Stock stock, Integer userId, boolean quickSearch) {
+    StockListVO stockListVO = SinaStockApi.assembleLideStockListVO(LiDeDataUtils.getStock(stock.getStockCode()));
+    if (ObjectUtils.isEmpty(stockListVO)) {
+      stockListVO = SinaStockApi.assembleStockListVO(SinaStockApi.getSinaStock(stock.getStockGid()));
+    }
+    if (stockListVO == null) {
+      stockListVO = new StockListVO();
+      stockListVO.setName(stock.getStockName());
+      stockListVO.setNowPrice("0");
+      stockListVO.setHcrate(java.math.BigDecimal.ZERO);
+    }
+    stockListVO.setCode(stock.getStockCode());
+    stockListVO.setSpell(stock.getStockSpell());
+    stockListVO.setGid(stock.getStockGid());
+    if (!quickSearch) {
+      BigDecimal day3Rate = (BigDecimal) selectRateByDaysAndStockCode(stock.getStockCode(), 3).getData();
+      stockListVO.setDay3Rate(day3Rate);
+    }
+    stockListVO.setStock_plate(stock.getStockPlate());
+    stockListVO.setStock_type(stock.getStockType());
+    if (userId == null) {
+      stockListVO.setIsOption("0");
+    } else if (quickSearch) {
+      stockListVO.setIsOption("0");
+    } else {
+      stockListVO.setIsOption(iStockOptionService.isMyOption(userId, stock.getStockCode()));
+    }
+    return stockListVO;
+  }
   public void z1() {
     this.stockPoll.z1();
   }
diff --git a/src/main/java/com/nq/service/impl/UserFundsPositionServiceImpl.java b/src/main/java/com/nq/service/impl/UserFundsPositionServiceImpl.java
index b2164d4..1c6c174 100644
--- a/src/main/java/com/nq/service/impl/UserFundsPositionServiceImpl.java
+++ b/src/main/java/com/nq/service/impl/UserFundsPositionServiceImpl.java
@@ -341,7 +341,7 @@
         userPosition.setIsLock(Integer.valueOf(0));
         userPosition.setOrderLever(lever);
         userPosition.setOrderTotalPrice(buy_amt);
-        BigDecimal buy_fee_amt = buy_amt.multiply(siteSetting.getBuyFee()).setScale(2, 4);
+        BigDecimal buy_fee_amt = com.nq.utils.TradeFeeUtil.calcBuyFee(buy_amt);
         log.info("用户购买手续费(配资后总资金 * 百分比) = {}", buy_fee_amt);
         userPosition.setOrderFee(buy_fee_amt);
 
diff --git a/src/main/java/com/nq/service/impl/UserPositionServiceImpl.java b/src/main/java/com/nq/service/impl/UserPositionServiceImpl.java
index 377b1fd..5162932 100644
--- a/src/main/java/com/nq/service/impl/UserPositionServiceImpl.java
+++ b/src/main/java/com/nq/service/impl/UserPositionServiceImpl.java
@@ -11,6 +11,8 @@
 import com.google.common.collect.Lists;
 import com.nq.common.ServerResponse;
 import com.nq.utils.*;
+import com.nq.utils.TradeFeeUtil;
+import com.nq.utils.redis.JsonUtil;
 import com.nq.utils.redis.RedisShardedPoolUtils;
 import com.nq.utils.stock.BuyAndSellUtils;
 import com.nq.utils.stock.GeneratePosition;
@@ -111,6 +113,7 @@
     StockDzMapper stockDzMapper;
 
 
+    @Override
     @Transactional
     public ServerResponse buy(Integer stockId, Integer buyNum, Integer buyType, Integer lever, BigDecimal profitTarget, BigDecimal stopTarget, HttpServletRequest request) throws Exception {
 
@@ -385,11 +388,12 @@
         }
 
 
-        int compareUserAmtInt = user_enable_amt.compareTo(buy_amt_autual);
-        log.info("用户可用金额 = {}  实际购买金额 =  {}", user_enable_amt, buy_amt_autual);
-        log.info("比较 用户金额 和 实际 购买金额 =  {}", Integer.valueOf(compareUserAmtInt));
+        BigDecimal buy_fee_amt = TradeFeeUtil.calcBuyFee(buy_amt);
+        BigDecimal buy_debit = TradeFeeUtil.calcBuyDebit(buy_amt_autual, buy_fee_amt);
+        int compareUserAmtInt = user_enable_amt.compareTo(buy_debit);
+        log.info("用户可用金额 = {}  下单扣款(保证金+手续费) =  {}", user_enable_amt, buy_debit);
         if (compareUserAmtInt == -1) {
-            return ServerResponse.createByErrorMsg("下单失败,融资可用金额小于" + buy_amt_autual + "元");
+            return ServerResponse.createByErrorMsg("下单失败,融资可用金额小于" + buy_debit + "元(含保证金及手续费)");
         }
 
         if (user.getUserIndexAmt().compareTo(new BigDecimal("0")) == -1) {
@@ -445,7 +449,6 @@
         userPosition.setOrderStayDays(1);
 
 
-        BigDecimal buy_fee_amt = buy_amt.multiply(siteSetting.getBuyFee()).setScale(2, 4);
         log.info("用户购买手续费(配资后总资金 * 百分比) = {}", buy_fee_amt);
         userPosition.setOrderFee(buy_fee_amt);
 
@@ -477,33 +480,11 @@
         userPosition.setOrderStayDays(Integer.valueOf(0));
         userPosition.setOrderStayFee(new BigDecimal("0"));
 
-        int insertPositionCount = 0;
         this.userPositionMapper.insert(userPosition);
-        insertPositionCount = userPosition.getId();
-        if (insertPositionCount > 0) {
-            //修改用户可用余额= 当前余额-下单金额-买入手续费-印花税-点差费
-            //BigDecimal reckon_enable = user_enable_amt.subtract(buy_amt_autual).subtract(buy_fee_amt).subtract(buy_yhs_amt).subtract(spread_rate_amt);
-            //修改用户可用余额= 当前余额-下单总金额
-            BigDecimal reckon_enable = user_enable_amt.subtract(buy_amt_autual);
-            //修改用户可取余额=当前可取余额-下单总金额
-            int compareUserWithdrawAmtInt = user_enable_withdraw_amt.compareTo(buy_amt_autual);
-            if (compareUserWithdrawAmtInt == -1) {
-                //若可取余额小于下单总额,但是可用余额充足,令可取余额为0
-                user.setEnaleWithdrawAmt(BigDecimal.ZERO);
-            } else {
-                user_enable_withdraw_amt = user_enable_withdraw_amt.subtract(buy_amt_autual);
-                user.setEnaleWithdrawAmt(user_enable_withdraw_amt);
-            }
-            user.setEnableAmt(reckon_enable);
-            int updateUserCount = this.userMapper.updateByPrimaryKeySelective(user);
-            if (updateUserCount > 0) {
-                log.info("【用户交易下单】修改用户金额成功");
-            } else {
-                log.error("用户交易下单】修改用户金额出错");
-                throw new Exception("用户交易下单】修改用户金额出错");
-            }
-            //核算代理收入-入仓手续费
+        if (userPosition.getId() != null && userPosition.getId() > 0) {
+            deductUserEnableOnBuy(user, buy_debit, buy_amt_autual, buy_fee_amt, userPosition);
             iAgentAgencyFeeService.AgencyFeeIncome(1, userPosition.getPositionSn());
+            syncUserCacheAfterTrade(request);
             log.info("【用户交易下单】保存持仓记录成功");
         } else {
             log.error("用户交易下单】保存持仓记录出错");
@@ -514,12 +495,111 @@
     }
 
 
-    public ServerResponse fee(Integer buyNum,BigDecimal nowPrice){
+    @Override
+    public ServerResponse fee(Integer buyNum, BigDecimal nowPrice) {
         BigDecimal buy_amt = nowPrice.multiply(new BigDecimal(buyNum.intValue()));
-        SiteSetting siteSetting = this.iSiteSettingService.getSiteSetting();
-        BigDecimal buy_fee_amt = buy_amt.multiply(siteSetting.getBuyFee()).setScale(2, 4);
+        BigDecimal buy_fee_amt = TradeFeeUtil.calcBuyFee(buy_amt).setScale(2, 4);
         return ServerResponse.createBySuccess(buy_fee_amt);
     }
+
+    /** 下单从两融可用资金扣除:保证金 + 买入手续费 */
+    private void deductUserEnableOnBuy(User user, BigDecimal buyDebit, BigDecimal margin, BigDecimal fee,
+                                       UserPosition position) throws Exception {
+        User fresh = this.userMapper.selectByPrimaryKey(user.getId());
+        if (fresh == null) {
+            throw new Exception("用户不存在");
+        }
+        BigDecimal enableAmt = fresh.getEnableAmt() == null ? BigDecimal.ZERO : fresh.getEnableAmt();
+        if (enableAmt.compareTo(buyDebit) < 0) {
+            throw new Exception("扣除可用资金失败(保证金+手续费=" + buyDebit + "元),余额不足");
+        }
+        fresh.setEnableAmt(enableAmt.subtract(buyDebit));
+        BigDecimal withdrawAmt = fresh.getEnaleWithdrawAmt() == null ? BigDecimal.ZERO : fresh.getEnaleWithdrawAmt();
+        if (withdrawAmt.compareTo(buyDebit) < 0) {
+            fresh.setEnaleWithdrawAmt(BigDecimal.ZERO);
+        } else {
+            fresh.setEnaleWithdrawAmt(withdrawAmt.subtract(buyDebit));
+        }
+        BigDecimal userAmt = fresh.getUserAmt() == null ? BigDecimal.ZERO : fresh.getUserAmt();
+        fresh.setUserAmt(userAmt.subtract(buyDebit));
+        int rows = this.userMapper.updateByPrimaryKeySelective(fresh);
+        if (rows <= 0) {
+            throw new Exception("扣除可用资金失败(保证金+手续费=" + buyDebit + "元)");
+        }
+        saveBuyDebitCashDetail(user, position, margin, fee, buyDebit);
+        log.info("【用户交易下单】扣款成功,用户={},保证金={},手续费={},合计={}", user.getId(), margin, fee, buyDebit);
+    }
+
+    private void refundUserEnableOnCancel(User user, BigDecimal refundAmt, UserPosition position) throws Exception {
+        User fresh = this.userMapper.selectByPrimaryKey(user.getId());
+        if (fresh == null) {
+            throw new Exception("用户不存在");
+        }
+        BigDecimal enableAmt = fresh.getEnableAmt() == null ? BigDecimal.ZERO : fresh.getEnableAmt();
+        BigDecimal withdrawAmt = fresh.getEnaleWithdrawAmt() == null ? BigDecimal.ZERO : fresh.getEnaleWithdrawAmt();
+        BigDecimal userAmt = fresh.getUserAmt() == null ? BigDecimal.ZERO : fresh.getUserAmt();
+        fresh.setEnableAmt(enableAmt.add(refundAmt));
+        fresh.setEnaleWithdrawAmt(withdrawAmt.add(refundAmt));
+        fresh.setUserAmt(userAmt.add(refundAmt));
+        int rows = this.userMapper.updateByPrimaryKeySelective(fresh);
+        if (rows <= 0) {
+            throw new Exception("撤单退款失败");
+        }
+        UserCashDetail ucd = new UserCashDetail();
+        ucd.setPositionId(position.getId());
+        ucd.setAgentId(user.getAgentId());
+        ucd.setAgentName(user.getAgentName());
+        ucd.setUserId(user.getId());
+        ucd.setUserName(user.getRealName());
+        ucd.setDeType("撤单退款");
+        ucd.setDeAmt(refundAmt);
+        ucd.setDeSummary("撤销委托," + position.getStockCode() + "/" + position.getStockName()
+                + ",退还保证金+手续费:" + refundAmt);
+        ucd.setAddTime(new Date());
+        ucd.setIsRead(Integer.valueOf(0));
+        this.userCashDetailMapper.insert(ucd);
+        log.info("【用户撤单】退款成功,用户={},金额={}", user.getId(), refundAmt);
+    }
+
+    private void saveBuyDebitCashDetail(User user, UserPosition position, BigDecimal margin, BigDecimal fee,
+                                        BigDecimal buyDebit) {
+        UserCashDetail ucd = new UserCashDetail();
+        ucd.setPositionId(position.getId());
+        ucd.setAgentId(user.getAgentId());
+        ucd.setAgentName(user.getAgentName());
+        ucd.setUserId(user.getId());
+        ucd.setUserName(user.getRealName());
+        ucd.setDeType("买入扣款");
+        ucd.setDeAmt(buyDebit.negate());
+        ucd.setDeSummary("委托买入," + position.getStockCode() + "/" + position.getStockName()
+                + ",保证金:" + margin + ",手续费:" + fee + ",合计扣款:" + buyDebit);
+        ucd.setAddTime(new Date());
+        ucd.setIsRead(Integer.valueOf(0));
+        this.userCashDetailMapper.insert(ucd);
+    }
+
+    /** 下单/撤单后刷新 Redis 中的用户资金,避免页面仍显示旧可用余额 */
+    private void syncUserCacheAfterTrade(HttpServletRequest request) {
+        if (request == null) {
+            return;
+        }
+        String cookieName = PropertiesUtil.getProperty("user.cookie.name");
+        String loginToken = request.getHeader(cookieName);
+        if (StringUtils.isBlank(loginToken)) {
+            return;
+        }
+        String userJson = RedisShardedPoolUtils.get(loginToken);
+        User cached = (User) JsonUtil.string2Obj(userJson, User.class);
+        if (cached == null || cached.getId() == null) {
+            return;
+        }
+        User dbUser = this.userMapper.selectByPrimaryKey(cached.getId());
+        if (dbUser != null) {
+            RedisShardedPoolUtils.setEx(loginToken, JsonUtil.obj2String(dbUser), 9999);
+        }
+    }
+
+    @Override
     @Transactional
     public ServerResponse pending(Integer stockId, Integer buyNum, Integer buyType, Integer lever, BigDecimal profitTarget, BigDecimal stopTarget, HttpServletRequest request) throws Exception {
 
@@ -708,11 +788,10 @@
                     .getBuyMaxAmtPercent().multiply(new BigDecimal("100")) + "%");
         }
 
-        int compareUserAmtInt = user_enable_amt.compareTo(buy_amt_autual);
-        log.info("用户可用金额 = {}  实际购买金额 =  {}", user_enable_amt, buy_amt_autual);
-        log.info("比较 用户金额 和 实际 购买金额 =  {}", Integer.valueOf(compareUserAmtInt));
-        if (compareUserAmtInt == -1) {
-            return ServerResponse.createByErrorMsg("挂单失败,融资可用金额小于" + buy_amt_autual + "元");
+        BigDecimal buy_fee_amt = TradeFeeUtil.calcBuyFee(buy_amt);
+        BigDecimal buy_debit = TradeFeeUtil.calcBuyDebit(buy_amt_autual, buy_fee_amt);
+        if (user_enable_amt.compareTo(buy_debit) == -1) {
+            return ServerResponse.createByErrorMsg("挂单失败,融资可用金额小于" + buy_debit + "元(含保证金及手续费)");
         }
         if (user.getUserIndexAmt().compareTo(new BigDecimal("0")) == -1) {
             return ServerResponse.createByErrorMsg("失败,指数总资金小于0");
@@ -751,7 +830,6 @@
         BigDecimal allStayFee = stayFee.multiply(new BigDecimal(1));
         userPosition.setOrderStayFee(allStayFee);
         userPosition.setOrderStayDays(1);
-        BigDecimal buy_fee_amt = buy_amt.multiply(siteSetting.getBuyFee()).setScale(2, 4);
         log.info("用户购买手续费(配资后总资金 * 百分比) = {}", buy_fee_amt);
         userPosition.setOrderFee(buy_fee_amt);
         BigDecimal buy_yhs_amt = buy_amt.multiply(siteSetting.getDutyFee()).setScale(2, 4);
@@ -772,40 +850,60 @@
         userPosition.setAllProfitAndLose(all_profit_and_lose);
         userPosition.setOrderStayDays(Integer.valueOf(0));
         userPosition.setOrderStayFee(new BigDecimal("0"));
-        int insertPositionCount = 0;
         this.userPositionMapper.insert(userPosition);
-        insertPositionCount = userPosition.getId();
-        if (insertPositionCount > 0) {
-            //修改用户可用余额= 当前余额-下单金额-买入手续费-印花税-点差费
-            //BigDecimal reckon_enable = user_enable_amt.subtract(buy_amt_autual).subtract(buy_fee_amt).subtract(buy_yhs_amt).subtract(spread_rate_amt);
-            //修改用户可用余额= 当前余额-下单总金额
-            BigDecimal reckon_enable = user_enable_amt.subtract(buy_amt_autual);
-            //修改用户可取余额=当前可取余额-下单总金额
-            int compareUserWithdrawAmtInt = user_enable_withdraw_amt.compareTo(buy_amt_autual);
-            if (compareUserWithdrawAmtInt == -1) {
-                //若可取余额小于下单总额,但是可用余额充足,令可取余额为0
-                user.setEnaleWithdrawAmt(BigDecimal.ZERO);
-            } else {
-                user_enable_withdraw_amt = user_enable_withdraw_amt.subtract(buy_amt_autual);
-                user.setEnaleWithdrawAmt(user_enable_withdraw_amt);
-            }
-            user.setEnableAmt(reckon_enable);
-//            user.setDjzj(user.getDjzj().subtract(buy_amt_autual));
-            int updateUserCount = this.userMapper.updateByPrimaryKeySelective(user);
-            if (updateUserCount > 0) {
-                log.info("【用户交易下单】修改用户金额成功");
-            } else {
-                log.error("用户交易下单】修改用户金额出错");
-                throw new Exception("用户交易下单】修改用户金额出错");
-            }
-            //核算代理收入-入仓手续费
-//            iAgentAgencyFeeService.AgencyFeeIncome(1, userPosition.getPositionSn());
+        if (userPosition.getId() != null && userPosition.getId() > 0) {
+            deductUserEnableOnBuy(user, buy_debit, buy_amt_autual, buy_fee_amt, userPosition);
+            syncUserCacheAfterTrade(request);
             log.info("【用户交易下单】保存持仓记录成功");
         } else {
             log.error("用户交易下单】保存持仓记录出错");
             throw new Exception("用户交易下单】保存持仓记录出错");
         }
         return ServerResponse.createBySuccess("挂单成功");
+    }
+
+    /**
+     * 撤销委托单(status=0),退还保证金+买入手续费
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public ServerResponse cancelOrder(String positionSn, HttpServletRequest request) throws Exception {
+        if (StringUtils.isBlank(positionSn)) {
+            return ServerResponse.createByErrorMsg("参数错误");
+        }
+        User user = this.iUserService.getCurrentRefreshUser(request);
+        if (user == null) {
+            return ServerResponse.createByErrorMsg("请先登录");
+        }
+        UserPosition userPosition = this.userPositionMapper.findPositionBySn(positionSn);
+        if (userPosition == null) {
+            return ServerResponse.createByErrorMsg("委托不存在");
+        }
+        if (!user.getId().equals(userPosition.getUserId())) {
+            return ServerResponse.createByErrorMsg("无权操作该委托");
+        }
+        if (userPosition.getStatus() == null || userPosition.getStatus().intValue() != 0) {
+            return ServerResponse.createByErrorMsg("当前订单不可撤单");
+        }
+        if (userPosition.getSellOrderId() != null) {
+            return ServerResponse.createByErrorMsg("当前订单不可撤单");
+        }
+        BigDecimal buyAmtActual = userPosition.getOrderTotalPrice()
+                .divide(new BigDecimal(userPosition.getOrderLever()), 2, 4);
+        BigDecimal buyFee = userPosition.getOrderFee() != null ? userPosition.getOrderFee() : BigDecimal.ZERO;
+        BigDecimal refundAmt = TradeFeeUtil.calcBuyDebit(buyAmtActual, buyFee);
+        User freshUser = this.userMapper.selectByPrimaryKey(user.getId());
+        if (freshUser == null) {
+            throw new Exception("用户不存在");
+        }
+        refundUserEnableOnCancel(freshUser, refundAmt, userPosition);
+        int delCount = this.userPositionMapper.deleteByPrimaryKey(userPosition.getId());
+        if (delCount <= 0) {
+            throw new Exception("撤单失败,删除委托记录失败");
+        }
+        syncUserCacheAfterTrade(request);
+        log.info("【用户撤单】positionSn={} 退还保证金+手续费={}", positionSn, refundAmt);
+        return ServerResponse.createBySuccessMsg("撤单成功");
     }
 
 
@@ -1055,8 +1153,8 @@
         BigDecimal sell_fee_amt = all_sell_amt.multiply(siteSetting.getSellFee()).setScale(2, 4);
         log.info("卖出手续费 = {}", sell_fee_amt);
 
-        //總手續費= 買入手續費+賣出手續費+印花稅+遞延費+點差費
-        BigDecimal all_fee_amt = buy_fee_amt.add(sell_fee_amt).add(orderSpread).add(orderStayFee).add(spreadRatePrice);
+        // 买入手续费已在下单时扣除,平仓只结算卖出侧费用
+        BigDecimal all_fee_amt = sell_fee_amt.add(orderSpread).add(orderStayFee).add(spreadRatePrice);
         log.info("总的手续费费用 = {}", all_fee_amt);
 
         userPosition.setSellOrderId(GeneratePosition.getPositionId());
@@ -1266,7 +1364,7 @@
         BigDecimal user_enable_amt = user.getEnableAmt();
         log.info("用戶原本總資金 = {} , 可用 = {}", user_all_amt, user_enable_amt);
 
-        BigDecimal buy_fee_amt = all_buy_amt.multiply(siteSetting.getBuyFee()).setScale(2,4);
+        BigDecimal buy_fee_amt = TradeFeeUtil.calcBuyFee(all_buy_amt);
         log.info("買入手續費 = {}", buy_fee_amt);
 
         BigDecimal orderSpread = all_buy_amt.multiply(siteSetting.getDutyFee()).setScale(2, 4);
@@ -1281,9 +1379,8 @@
         BigDecimal sell_fee_amt = all_sell_amt.multiply(siteSetting.getSellFee()).setScale(2, 4);
         log.info("賣出手續費 = {}", sell_fee_amt);
 
-        //總手續費= 買入手續費+賣出手續費+印花稅+遞延費+點差費
-//        BigDecimal all_fee_amt = buy_fee_amt.add(sell_fee_amt).add(orderSpread).add(orderStayFee).add(spreadRatePrice);
-        BigDecimal all_fee_amt = buy_fee_amt.add(sell_fee_amt).add(orderSpread);
+        // 买入手续费已在下单时扣除
+        BigDecimal all_fee_amt = sell_fee_amt.add(orderSpread);
         log.info("總的手續費費用 = {}", all_fee_amt);
         //复制一条新订单
         UserPosition userPositionNew = new UserPosition();
@@ -1425,7 +1522,8 @@
         }
 
 
-        userPosition.setMarginAdd(userPosition.getMarginAdd().add(marginAdd));
+        BigDecimal existMarginAdd = userPosition.getMarginAdd() == null ? BigDecimal.ZERO : userPosition.getMarginAdd();
+        userPosition.setMarginAdd(existMarginAdd.add(marginAdd));
 
         int updatePositionCount = this.userPositionMapper.updateByPrimaryKeySelective(userPosition);
         if (updatePositionCount > 0) {
@@ -1823,13 +1921,11 @@
         BigDecimal buy_amt_autual = buy_amt.divide(new BigDecimal(lever.intValue()), 2, 4);
 
 
-        int compareUserAmtInt = user_enable_amt.compareTo(buy_amt_autual);
-        log.info("用户可用金额 = {}  实际购买金额 =  {}", user_enable_amt, buy_amt_autual);
-        log.info("比较 用户金额 和 实际 购买金额 =  {}", Integer.valueOf(compareUserAmtInt));
-        if (compareUserAmtInt == -1) {
-            log.info("下单失败,用户可用金额小于" + buy_amt_autual + "元");
-            return ServerResponse.createByErrorMsg("下单失败,用户可用金额小于" + buy_amt_autual + "元");
-
+        BigDecimal buy_fee_amt_check = TradeFeeUtil.calcBuyFee(buy_amt);
+        BigDecimal buy_debit_check = TradeFeeUtil.calcBuyDebit(buy_amt_autual, buy_fee_amt_check);
+        if (user_enable_amt.compareTo(buy_debit_check) < 0) {
+            log.info("下单失败,用户可用金额小于{}元(含保证金及手续费)", buy_debit_check);
+            return ServerResponse.createByErrorMsg("下单失败,用户可用金额小于" + buy_debit_check + "元(含保证金及手续费)");
         }
 
         if (user.getUserIndexAmt().compareTo(new BigDecimal("0")) == -1) {
@@ -1886,7 +1982,7 @@
         userPosition.setOrderStayDays(1);
 
 
-        BigDecimal buy_fee_amt = buy_amt.multiply(siteSetting.getBuyFee()).setScale(2, 4);
+        BigDecimal buy_fee_amt = TradeFeeUtil.calcBuyFee(buy_amt).setScale(2, 4);
         log.info("创建模拟持仓 手续费(配资后总资金 * 百分比) = {}", buy_fee_amt);
         userPosition.setOrderFee(buy_fee_amt);
 
@@ -1931,20 +2027,19 @@
         userPosition.setOrderStayFee(new BigDecimal("0"));
         userPosition.setSpreadRatePrice(new BigDecimal("0"));
 
-        int insertPositionCount = this.userPositionMapper.insert(userPosition);
-        if (insertPositionCount > 0) {
-            log.info("【创建持仓】保存记录成功");
-        } else {
+        this.userPositionMapper.insert(userPosition);
+        if (userPosition.getId() == null || userPosition.getId() <= 0) {
             log.error("【创建持仓】保存记录出错");
+            return ServerResponse.createByErrorMsg("生成持仓失败");
         }
-        BigDecimal reckon_enable = user_enable_amt.subtract(buy_amt_autual);
-        user.setEnableAmt(reckon_enable);
-        int updateUserCount = this.userMapper.updateByPrimaryKeySelective(user);
-        if (updateUserCount > 0) {
-            log.info("【用户交易下单】修改用户金额成功");
-        } else {
-            log.error("用户交易下单】修改用户金额出错");
-
+        log.info("【创建持仓】保存记录成功");
+        BigDecimal buy_debit = TradeFeeUtil.calcBuyDebit(buy_amt_autual, buy_fee_amt);
+        try {
+            deductUserEnableOnBuy(user, buy_debit, buy_amt_autual, buy_fee_amt, userPosition);
+        } catch (Exception e) {
+            this.userPositionMapper.deleteByPrimaryKey(userPosition.getId());
+            log.error("【创建持仓】扣款失败,已回滚持仓记录", e);
+            return ServerResponse.createByErrorMsg("生成持仓失败:" + e.getMessage());
         }
         iAgentAgencyFeeService.AgencyFeeIncome(1, userPosition.getPositionSn());
         return ServerResponse.createBySuccess("生成持仓成功");
@@ -2375,7 +2470,7 @@
             userPosition.setOrderStayDays(1);
             userPosition.setOrderTotalPrice(userStockSubscribe.getBond());
 
-            //            BigDecimal buy_fee_amt = buy_amt.multiply(siteSetting.getBuyFee()).setScale(2, 4);
+            //            BigDecimal buy_fee_amt = TradeFeeUtil.calcBuyFee(buy_amt).setScale(2, 4);
             BigDecimal buy_fee_amt = new BigDecimal(0);
             log.info("用戶購買手續費(配資後總資金 * 百分比) = {}", buy_fee_amt);
             userPosition.setOrderFee(buy_fee_amt);
@@ -2649,11 +2744,10 @@
         }
 
 
-        int compareUserAmtInt = user_enable_amt.compareTo(buy_amt_autual);
-        log.info("用戶可用金額 = {}  實際購買金額 =  {}", user_enable_amt, buy_amt_autual);
-        log.info("比較 用戶金額 和 實際 購買金額 =  {}", Integer.valueOf(compareUserAmtInt));
-        if (compareUserAmtInt == -1) {
-            return ServerResponse.createByErrorMsg("下单失败,融资可用金额小于" + buy_amt_autual + "元");
+        BigDecimal buy_fee_amt_dz = TradeFeeUtil.calcBuyFee(buy_amt);
+        BigDecimal buy_debit_dz = TradeFeeUtil.calcBuyDebit(buy_amt_autual, buy_fee_amt_dz);
+        if (user_enable_amt.compareTo(buy_debit_dz) == -1) {
+            return ServerResponse.createByErrorMsg("下单失败,融资可用金额小于" + buy_debit_dz + "元(含保证金及手续费)");
         }
 
 //        if (user.getUserIndexAmt().compareTo(new BigDecimal("0")) == -1) {
@@ -2690,7 +2784,7 @@
         userPosition.setOrderStayDays(1);
 
 
-        BigDecimal buy_fee_amt = buy_amt.multiply(siteSetting.getBuyFee()).setScale(2, 4);
+        BigDecimal buy_fee_amt = buy_fee_amt_dz;
         log.info("用戶購買手續費(配資後總資金 * 百分比) = {}", buy_fee_amt);
         userPosition.setOrderFee(buy_fee_amt);
 
@@ -2723,36 +2817,14 @@
 
         log.info("--------------购买逻辑股票数据 buyDz  stock------" + new Gson().toJson(userPosition));
 
-        int insertPositionCount = 0;
         this.userPositionMapper.insert(userPosition);
-        insertPositionCount = userPosition.getId();
-        if (insertPositionCount > 0) {
+        if (userPosition.getId() != null && userPosition.getId() > 0) {
             //修改大宗剩余
             stockDz.setStockShare(stockDz.getStockShare() - num);
             stockDz.setStockSurplus(stockDz.getStockSurplus() + num);
             stockDzMapper.updateById(stockDz);
-            //修改用戶可用余額= 當前余額-下單金額-買入手續費-印花稅-點差費
-            //BigDecimal reckon_enable = user_enable_amt.subtract(buy_amt_autual).subtract(buy_fee_amt).subtract(buy_yhs_amt).subtract(spread_rate_amt);
-            //修改用戶可用余額= 當前余額-下單總金額
-            BigDecimal reckon_enable = user_enable_amt.subtract(buy_amt_autual);
-            //修改用戶可取余額=當前可取余額-下單總金額
-            int compareUserWithdrawAmtInt = user_enable_withdraw_amt.compareTo(buy_amt_autual);
-            if (compareUserWithdrawAmtInt == -1) {
-                //若可取余額小於下單總額,但是可用余額充足,令可取余額為0
-                user.setEnaleWithdrawAmt(BigDecimal.ZERO);
-            } else {
-                user_enable_withdraw_amt = user_enable_withdraw_amt.subtract(buy_amt_autual);
-                user.setEnaleWithdrawAmt(user_enable_withdraw_amt);
-            }
-            user.setEnableAmt(reckon_enable);
-            int updateUserCount = this.userMapper.updateByPrimaryKeySelective(user);
-            if (updateUserCount > 0) {
-                log.info("【用戶交易下單】修改用戶金額成功");
-            } else {
-                log.error("用戶交易下單】修改用戶金額出錯");
-                throw new Exception("用戶交易下單】修改用戶金額出錯");
-            }
-            //核算代理收入-入倉手續費
+            deductUserEnableOnBuy(user, buy_debit_dz, buy_amt_autual, buy_fee_amt_dz, userPosition);
+            syncUserCacheAfterTrade(request);
             iAgentAgencyFeeService.AgencyFeeIncome(1, userPosition.getPositionSn());
             log.info("【用戶交易下單】保存持倉記錄成功");
         } else {
@@ -2980,11 +3052,10 @@
         }
 
 
-        int compareUserAmtInt = user_enable_amt.compareTo(buy_amt_autual);
-        log.info("用戶可用金額 = {}  實際購買金額 =  {}", user_enable_amt, buy_amt_autual);
-        log.info("比較 用戶金額 和 實際 購買金額 =  {}", Integer.valueOf(compareUserAmtInt));
-        if (compareUserAmtInt == -1) {
-            return ServerResponse.createByErrorMsg("下單失敗,可用金額小於" + buy_amt_autual + "元");
+        BigDecimal buy_fee_amt = TradeFeeUtil.calcBuyFee(buy_amt);
+        BigDecimal buy_debit = TradeFeeUtil.calcBuyDebit(buy_amt_autual, buy_fee_amt);
+        if (user_enable_amt.compareTo(buy_debit) == -1) {
+            return ServerResponse.createByErrorMsg("下單失敗,可用金額小於" + buy_debit + "元(含保证金及手续费)");
         }
 
 //        if (user.getUserIndexAmt().compareTo(new BigDecimal("0")) == -1) {
@@ -3038,7 +3109,6 @@
         userPosition.setOrderStayDays(1);
 
 
-        BigDecimal buy_fee_amt = buy_amt.multiply(siteSetting.getBuyFee()).setScale(2, 4);
         log.info("用戶購買手續費(配資後總資金 * 百分比) = {}", buy_fee_amt);
         userPosition.setOrderFee(buy_fee_amt);
 
@@ -3072,31 +3142,10 @@
 
         log.info("--------------购买逻辑股票数据 buyVipQc  stock------" + new Gson().toJson(userPosition));
 
-        int insertPositionCount = 0;
         this.userPositionMapper.insert(userPosition);
-        insertPositionCount = userPosition.getId();
-        if (insertPositionCount > 0) {
-            //修改用戶可用余額= 當前余額-下單金額-買入手續費-印花稅-點差費
-            //BigDecimal reckon_enable = user_enable_amt.subtract(buy_amt_autual).subtract(buy_fee_amt).subtract(buy_yhs_amt).subtract(spread_rate_amt);
-            //修改用戶可用余額= 當前余額-下單總金額
-            BigDecimal reckon_enable = user_enable_amt.subtract(buy_amt_autual);
-            //修改用戶可取余額=當前可取余額-下單總金額
-            int compareUserWithdrawAmtInt = user_enable_withdraw_amt.compareTo(buy_amt_autual);
-            if (compareUserWithdrawAmtInt < 0) {
-                //若可取余額小於下單總額,但是可用余額充足,令可取余額為0
-                user.setEnaleWithdrawAmt(BigDecimal.ZERO);
-            } else {
-                user_enable_withdraw_amt = user_enable_withdraw_amt.subtract(buy_amt_autual);
-                user.setEnaleWithdrawAmt(user_enable_withdraw_amt);
-            }
-            user.setEnableAmt(reckon_enable);
-            int updateUserCount = this.userMapper.updateByPrimaryKeySelective(user);
-            if (updateUserCount > 0) {
-                log.info("【用戶交易下單】修改用戶金額成功");
-            } else {
-                log.error("用戶交易下單】修改用戶金額出錯");
-                throw new Exception("用戶交易下單】修改用戶金額出錯");
-            }
+        if (userPosition.getId() != null && userPosition.getId() > 0) {
+            deductUserEnableOnBuy(user, buy_debit, buy_amt_autual, buy_fee_amt, userPosition);
+            syncUserCacheAfterTrade(request);
             //核算代理收入-入倉手續費
             //iAgentAgencyFeeService.AgencyFeeIncome(1, userPosition.getPositionSn());
             log.info("【用戶交易下單】保存持倉記錄成功");
diff --git a/src/main/java/com/nq/service/impl/UserServiceImpl.java b/src/main/java/com/nq/service/impl/UserServiceImpl.java
index cfbc8b4..59bb0d5 100644
--- a/src/main/java/com/nq/service/impl/UserServiceImpl.java
+++ b/src/main/java/com/nq/service/impl/UserServiceImpl.java
@@ -310,18 +310,11 @@
                 stock.setStockName(stockFutures.getFuturesName());
                 stock.setIsLock(0);
             }
-        } else if(code.contains("sh") || code.contains("sz")){
-         return ServerResponse.createByErrorMsg("添加失败,指数不支持自选");
-//            StockIndex stockIndex = this.stockIndexMapper.selectIndexByCode(stockcode);
-//            if(stockIndex != null){
-//                stock.setId(stockIndex.getId());
-//                stock.setStockCode(stockIndex.getIndexCode());
-//                stock.setStockGid(stockIndex.getIndexGid()+"zs");
-//                stock.setStockName(stockIndex.getIndexName());
-//                stock.setIsLock(0);
-//            }
         } else {
-            stock = this.stockMapper.findStockByCode(code);
+            stock = this.stockMapper.findStockByCode(stockcode);
+            if (stock == null && !code.equals(stockcode)) {
+                stock = this.stockMapper.findStockByCode(code);
+            }
         }
         if (stock == null) {
             return ServerResponse.createByErrorMsg("添加失败,股票不存在");
@@ -400,6 +393,26 @@
         return ServerResponse.createBySuccess(userInfoVO);
     }
 
+    @Override
+    public void syncUserCache(HttpServletRequest request) {
+        if (request == null) {
+            return;
+        }
+        String cookieName = PropertiesUtil.getProperty("user.cookie.name");
+        String loginToken = request.getHeader(cookieName);
+        if (StringUtils.isBlank(loginToken)) {
+            return;
+        }
+        String userJson = RedisShardedPoolUtils.get(loginToken);
+        User cached = (User) JsonUtil.string2Obj(userJson, User.class);
+        if (cached == null || cached.getId() == null) {
+            return;
+        }
+        User dbUser = this.userMapper.selectByPrimaryKey(cached.getId());
+        if (dbUser != null) {
+            RedisShardedPoolUtils.setEx(loginToken, JsonUtil.obj2String(dbUser), 9999);
+        }
+    }
 
     public ServerResponse updatePwd(String oldPwd, String newPwd, HttpServletRequest request) {
         if (StringUtils.isBlank(oldPwd) || StringUtils.isBlank(newPwd)) {
@@ -1874,10 +1887,20 @@
         BigDecimal allProfitAndLose = positionVO.getAllProfitAndLose();
         userInfoVO.setAllProfitAndLose(allProfitAndLose);
 
-//        BigDecimal userAllAmt = user.getUserAmt();
-        BigDecimal userAllAmt = user.getEnableAmt();
-        userAllAmt = userAllAmt.add(allProfitAndLose);
-
+        // 账户总资产 = 两融可用 + 冻结保证金 + 浮动盈亏
+        // 浮动盈亏里已扣过买入手续费,而买入手续费下单时已从 enableAmt 扣除,此处加回 openBuyFees 避免总资产「少扣手续费」
+        BigDecimal allFreezAmt = positionVO.getAllFreezAmt() == null ? BigDecimal.ZERO : positionVO.getAllFreezAmt();
+        BigDecimal enableAmt = user.getEnableAmt() == null ? BigDecimal.ZERO : user.getEnableAmt();
+        BigDecimal openBuyFees = BigDecimal.ZERO;
+        List<UserPosition> openPositions = this.iUserPositionService.findPositionByUserIdAndSellIdIsNull(user.getId());
+        if (openPositions != null) {
+            for (UserPosition position : openPositions) {
+                if (position.getOrderFee() != null) {
+                    openBuyFees = openBuyFees.add(position.getOrderFee());
+                }
+            }
+        }
+        BigDecimal userAllAmt = enableAmt.add(allFreezAmt).add(allProfitAndLose).add(openBuyFees);
 
         userInfoVO.setEnableIndexAmt(user.getEnableIndexAmt());
         userInfoVO.setEnaleWithdrawAmt(user.getEnaleWithdrawAmt());
@@ -1897,8 +1920,7 @@
             }
         }
         userInfoVO.setBuyAmtAutual(buyAmtAutual);
-        userAllAmt = userAllAmt.add(buyAmtAutual);
-        userInfoVO.setUserAmt(userAllAmt);
+        userInfoVO.setUserAmt(userAllAmt.setScale(2, RoundingMode.HALF_UP));
 
         List<UserPosition> userPositions = this.userPositionMapper.findMyPositionByCodeAndSpell(user.getId(), "", "", 2);
 
diff --git a/src/main/java/com/nq/service/impl/UserStockSubscribeServiceImpl.java b/src/main/java/com/nq/service/impl/UserStockSubscribeServiceImpl.java
index 25d7cba..636d57c 100644
--- a/src/main/java/com/nq/service/impl/UserStockSubscribeServiceImpl.java
+++ b/src/main/java/com/nq/service/impl/UserStockSubscribeServiceImpl.java
@@ -814,7 +814,7 @@
                 userPosition.setOrderStayDays(1);
 
 
-                BigDecimal buy_fee_amt = buy_amt.multiply(siteSetting.getBuyFee()).setScale(2, 4);
+                BigDecimal buy_fee_amt = com.nq.utils.TradeFeeUtil.calcBuyFee(buy_amt);
                 log.info("创建模拟持仓 手续费(配资后总资金 * 百分比) = {}", buy_fee_amt);
                 userPosition.setOrderFee(buy_fee_amt);
 
diff --git a/src/main/java/com/nq/utils/stock/sina/SinaStockApi.java b/src/main/java/com/nq/utils/stock/sina/SinaStockApi.java
index 8ed4485..9364539 100644
--- a/src/main/java/com/nq/utils/stock/sina/SinaStockApi.java
+++ b/src/main/java/com/nq/utils/stock/sina/SinaStockApi.java
@@ -37,17 +37,25 @@
     public static String getSinaStock(String stockGid) {
         String sina_result = "";
         try {
-//            System.out.println(sina_url + stockGid);
-//            sina_result = HttpClientRequest.doGet(sina_url + stockGid);
-//            System.out.println("请求返回:"+sina_result);
             System.out.println(PropertiesUtil.getProperty("sina.single.stock.proxy.url") + stockGid);
             sina_result = HttpClientRequest.doGet(PropertiesUtil.getProperty("sina.single.stock.proxy.url") + stockGid);
-            System.out.println("请求返回:"+sina_result);
-//            sina_result = "var hq_str_sz300270=\"中威电子,0.000,11.710,0.000,0.000,0.000,0.000,0.000,0,0.000,0,0.000,0,0.000,0,0.000,0,0.000,0,0.000,0,0.000,0,0.000,0,0.000,0,0.000,0,0.000,2025-12-03,09:10:06,00\";";
+            System.out.println("请求返回:" + sina_result);
         } catch (Exception e) {
-            log.error("获取股票行情出错,错误信息 = {}", e);
+            log.error("获取股票行情出错,gid={},错误信息 = {}", stockGid, e);
         }
-        return sina_result.substring(sina_result.indexOf("=") + 2);
+        if (StringUtils.isBlank(sina_result) || !sina_result.contains("=")) {
+            log.warn("新浪行情返回为空或格式异常, gid={}, raw={}", stockGid, StringUtils.abbreviate(sina_result, 200));
+            return "";
+        }
+        String body = sina_result.substring(sina_result.indexOf("=") + 2);
+        return body.replace("\"", "").replace(";", "").trim();
+    }
+
+    private static String hqField(String[] hqarr, int index) {
+        if (hqarr == null || index < 0 || index >= hqarr.length) {
+            return "0";
+        }
+        return StringUtils.defaultIfBlank(hqarr[index], "0");
     }
 
 
@@ -198,57 +206,57 @@
 
     public static StockVO assembleStockVO(String sinaResult) {
         StockVO stockVO = new StockVO();
-
+        if (StringUtils.isBlank(sinaResult)) {
+            return stockVO;
+        }
         String[] hqarr = sinaResult.split(",");
+        if (hqarr.length < 4) {
+            log.warn("新浪行情字段不足,无法解析,length={}, raw={}", hqarr.length, StringUtils.abbreviate(sinaResult, 200));
+            return stockVO;
+        }
 
-        stockVO.setName(hqarr[0]);
-
-        stockVO.setNowPrice(hqarr[3]);
+        stockVO.setName(hqField(hqarr, 0));
+        stockVO.setNowPrice(hqField(hqarr, 3));
 
         BigDecimal chang_rate = new BigDecimal("0");
-        if ((new BigDecimal(hqarr[2])).compareTo(new BigDecimal("0")) != 0 && new BigDecimal(hqarr[3]).compareTo(new BigDecimal("0")) != 0) {
-
-            chang_rate = (new BigDecimal(hqarr[3])).subtract(new BigDecimal(hqarr[2]));
-
-            chang_rate = chang_rate.multiply(new BigDecimal("100")).divide(new BigDecimal(hqarr[2]), 2, RoundingMode.HALF_UP);
+        BigDecimal preclose = new BigDecimal(hqField(hqarr, 2));
+        BigDecimal now = new BigDecimal(hqField(hqarr, 3));
+        if (preclose.compareTo(BigDecimal.ZERO) != 0 && now.compareTo(BigDecimal.ZERO) != 0) {
+            chang_rate = now.subtract(preclose);
+            chang_rate = chang_rate.multiply(new BigDecimal("100")).divide(preclose, 2, RoundingMode.HALF_UP);
         }
         stockVO.setHcrate(chang_rate);
 
-        stockVO.setToday_max(hqarr[4]);
+        stockVO.setToday_max(hqField(hqarr, 4));
+        stockVO.setToday_min(hqField(hqarr, 5));
+        stockVO.setBusiness_amount(hqField(hqarr, 8));
+        stockVO.setBusiness_balance(hqField(hqarr, 9));
+        stockVO.setPreclose_px(hqField(hqarr, 2));
+        stockVO.setOpen_px(hqField(hqarr, 1));
 
-        stockVO.setToday_min(hqarr[5]);
+        stockVO.setBuy1(hqField(hqarr, 6));
+        stockVO.setBuy2(hqField(hqarr, 13));
+        stockVO.setBuy3(hqField(hqarr, 15));
+        stockVO.setBuy4(hqField(hqarr, 17));
+        stockVO.setBuy5(hqField(hqarr, 19));
 
-        stockVO.setBusiness_amount(hqarr[8]);
+        stockVO.setSell1(hqField(hqarr, 7));
+        stockVO.setSell2(hqField(hqarr, 23));
+        stockVO.setSell3(hqField(hqarr, 25));
+        stockVO.setSell4(hqField(hqarr, 27));
+        stockVO.setSell5(hqField(hqarr, 29));
 
-        stockVO.setBusiness_balance(hqarr[9]);
+        stockVO.setBuy1_num(hqField(hqarr, 10));
+        stockVO.setBuy2_num(hqField(hqarr, 12));
+        stockVO.setBuy3_num(hqField(hqarr, 14));
+        stockVO.setBuy4_num(hqField(hqarr, 16));
+        stockVO.setBuy5_num(hqField(hqarr, 18));
 
-        stockVO.setPreclose_px(hqarr[2]);
-
-        stockVO.setOpen_px(hqarr[1]);
-
-        stockVO.setBuy1(hqarr[6]);
-        stockVO.setBuy2(hqarr[13]);
-        stockVO.setBuy3(hqarr[15]);
-        stockVO.setBuy4(hqarr[17]);
-        stockVO.setBuy5(hqarr[19]);
-
-        stockVO.setSell1(hqarr[7]);
-        stockVO.setSell2(hqarr[23]);
-        stockVO.setSell3(hqarr[25]);
-        stockVO.setSell4(hqarr[27]);
-        stockVO.setSell5(hqarr[29]);
-
-        stockVO.setBuy1_num(hqarr[10]);
-        stockVO.setBuy2_num(hqarr[12]);
-        stockVO.setBuy3_num(hqarr[14]);
-        stockVO.setBuy4_num(hqarr[16]);
-        stockVO.setBuy5_num(hqarr[18]);
-
-        stockVO.setSell1_num(hqarr[20]);
-        stockVO.setSell2_num(hqarr[22]);
-        stockVO.setSell3_num(hqarr[24]);
-        stockVO.setSell4_num(hqarr[26]);
-        stockVO.setSell5_num(hqarr[28]);
+        stockVO.setSell1_num(hqField(hqarr, 20));
+        stockVO.setSell2_num(hqField(hqarr, 22));
+        stockVO.setSell3_num(hqField(hqarr, 24));
+        stockVO.setSell4_num(hqField(hqarr, 26));
+        stockVO.setSell5_num(hqField(hqarr, 28));
 
         return stockVO;
     }
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 1c434fb..4812e0d 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -217,7 +217,16 @@
 
 
 #?????
-website.domain.url=http://www.huijuwang888.com
+# Ococn ??
+ococn.pay.pid=185583446
+ococn.pay.key=JcMJIbNUAcq0GyMf
+ococn.pay.url=https://pay.ococn.cn/submit.php
+ococn.pay.sitename=
+ococn.pay.name=账户充值
+ococn.pay.frontend_redirect=https://www.zhonghenginvest.com/#/user
+
+website.domain.url=https://api.zhonghenginvest.com
+frontend.domain.url=https://www.zhonghenginvest.com
 website.token=0DC8F78384C7AAFF3192A9C60A473FEE7F89C62888689616B98A06910E86B510
 
 #?????

--
Gitblit v1.9.3