商城代码同步

master
miaoqingshuai 2026-04-14 15:28:44 +08:00
parent 613300778f
commit 063a13f497
48 changed files with 3547 additions and 39 deletions

View File

@ -30,6 +30,8 @@
<artifactId>springfox-boot-starter</artifactId>
</dependency>
<!-- 防止进入swagger页面报类型转换错误排除3.0.0中的引用手动增加1.6.2版本 -->
<dependency>
<groupId>io.swagger</groupId>
@ -61,6 +63,23 @@
<artifactId>ruoyi-generator</artifactId>
</dependency>
<!-- Hutool工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.37</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.5</version>
</dependency>
</dependencies>

View File

@ -0,0 +1,58 @@
package com.ruoyi.web.controller;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.web.domain.Device;
import com.ruoyi.web.service.DeviceService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
*
*/
@RestController
@RequestMapping("/mall/device")
public class DeviceController extends BaseController {
@Resource
private DeviceService deviceService;
/**
*
*
* @return
*/
@Anonymous
@GetMapping(value = "/getDevice/{storeId}")
public AjaxResult getBrandTree(@PathVariable String storeId) {
return AjaxResult.success(deviceService.getDevice(storeId));
}
/**
*
*
* @return
*/
@Anonymous
@GetMapping(value = "/getSheXiangTou/{storeId}")
public AjaxResult getSheXiangTou(@PathVariable String storeId) {
return AjaxResult.success(deviceService.getSheXiangTou(storeId));
}
/**
*
*
* @param device
* @return
*/
@Anonymous
@PostMapping("/add")
public AjaxResult addDevice(@RequestBody Device device) {
device.setCreateBy(getUsername());
return toAjax(deviceService.addDevice(device));
}
}

View File

@ -0,0 +1,68 @@
package com.ruoyi.web.controller;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.web.domain.DoorScanRecord;
import com.ruoyi.web.domain.Order;
import com.ruoyi.web.service.DoorScanRecordService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.HashMap;
/**
*
*/
@RestController
@RequestMapping("/mall/doorScanRecord")
public class DoorScanRecordController extends BaseController {
@Autowired
private DoorScanRecordService doorScanRecordService;
/**
*
*/
@Anonymous
@Log(title = "扫码记录管理", businessType = BusinessType.INSERT)
@PostMapping("/add")
public AjaxResult addDoorScanRecord(@RequestBody DoorScanRecord doorScanRecord) {
doorScanRecord.setCreateBy(getUsername());
doorScanRecord.setScanTime(new Date());
return toAjax(doorScanRecordService.addDoorScanRecord(doorScanRecord));
}
/**
*
*
* @param doorScanRecord
* @return
*/
@Anonymous
@GetMapping("/list")
public AjaxResult list(DoorScanRecord doorScanRecord) {
return AjaxResult.success(doorScanRecordService.selectScanRecordList(doorScanRecord));
}
/**
*
*
* @param date (: yyyy-MM-dd)
* @return
*/
@GetMapping("/count")
public AjaxResult getEnterCount(@RequestParam String date,@RequestParam Integer storeId) {
try {
Integer count = doorScanRecordService.getEnterCountByDate(date,storeId);
return AjaxResult.success(count);
} catch (IllegalArgumentException e) {
return AjaxResult.error(e.getMessage());
}
}
}

View File

@ -0,0 +1,105 @@
package com.ruoyi.web.controller;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.web.domain.InstallTask;
import com.ruoyi.web.service.InstallTaskService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
*
*/
@RestController
@RequestMapping("/mall/install/task")
public class InstallTaskController extends BaseController {
@Autowired
private InstallTaskService installTaskService;
/**
*
*/
@PostMapping("/submit")
public AjaxResult submit(@RequestBody InstallTask installTask) {
installTask.setCreateBy(SecurityUtils.getUsername());
return toAjax(installTaskService.insertInstallTask(installTask));
}
/**
*
*/
@Anonymous
// @PreAuthorize("@ss.hasPermi('mall:install:list')")
@GetMapping("/list")
public TableDataInfo list(InstallTask installTask) {
startPage();
List<InstallTask> list = installTaskService.selectInstallTaskList(installTask);
return getDataTable(list);
}
/**
* ID
*/
@GetMapping("/{id}")
public AjaxResult getInfo(@PathVariable Long id) {
return AjaxResult.success(installTaskService.selectInstallTaskById(id));
}
/**
*
*/
@PreAuthorize("@ss.hasPermi('mall:install:edit')")
@PutMapping("/edit")
public AjaxResult edit(@RequestBody InstallTask installTask) {
installTask.setUpdateBy(SecurityUtils.getUsername());
return toAjax(installTaskService.updateInstallTask(installTask));
}
/**
*
*/
@PreAuthorize("@ss.hasPermi('mall:install:assign')")
@PostMapping("/assign")
public AjaxResult assign(@RequestParam Long id,
@RequestParam Long assigneeId,
@RequestParam String assigneeName) {
return toAjax(installTaskService.assignTask(id, assigneeId, assigneeName,
SecurityUtils.getUsername()));
}
/**
*
*/
@PreAuthorize("@ss.hasPermi('mall:install:complete')")
@PostMapping("/complete")
public AjaxResult complete(@RequestBody InstallTask installTask) {
installTask.setUpdateBy(SecurityUtils.getUsername());
return toAjax(installTaskService.completeTask(installTask));
}
/**
*
*/
@PreAuthorize("@ss.hasPermi('mall:install:remove')")
@DeleteMapping("/{id}")
public AjaxResult remove(@PathVariable Long id) {
return toAjax(installTaskService.deleteInstallTaskById(id));
}
/**
*
*/
@PreAuthorize("@ss.hasPermi('mall:install:remove')")
@DeleteMapping("/batch/{ids}")
public AjaxResult batchRemove(@PathVariable Long[] ids) {
return toAjax(installTaskService.deleteInstallTaskByIds(ids));
}
}

View File

@ -5,18 +5,26 @@ import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.web.domain.Order;
import com.ruoyi.web.dto.CreateOrderDTO;
import com.ruoyi.web.dto.ScanPayRequest;
import com.ruoyi.web.dto.TianquePayCallbackDTO;
import com.ruoyi.web.dto.TradeQueryDTO;
import com.ruoyi.web.service.OrderService;
import org.checkerframework.checker.units.qual.A;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
/**
*
*
*/
@RestController
@RequestMapping("/mall/order")
@ -25,14 +33,124 @@ public class OrderController extends BaseController {
@Resource
private OrderService orderService;
private static final Logger log = LoggerFactory.getLogger(OrderController.class);
/**
*
*
* @return
*/
@Anonymous
@GetMapping(value = "/getOrderCount/{storeId}")
public AjaxResult getOrderCount(@PathVariable String storeId) {
return AjaxResult.success(orderService.getOrderCount(storeId));
}
/**
* 使
*/
@Anonymous
@PostMapping("/tradeQuery")
public AjaxResult tradeQuery(@RequestBody TradeQueryDTO tradeQueryDTO) throws Exception {
return orderService.tradeQuery(tradeQueryDTO);
}
/**
*
*/
@Anonymous
@PostMapping("/addOrder")
public AjaxResult addOrder(@Validated @RequestBody CreateOrderDTO createOrderDTO) {
createOrderDTO.setCreateBy(getUsername());
return orderService.addOrder(createOrderDTO);
}
/**
*
*/
@Anonymous
@PostMapping("/addOrderReverseScan")
public AjaxResult addOrderReverseScan(@RequestBody ScanPayRequest scanPayRequest) throws Exception {
scanPayRequest.setCreateBy(getUsername());
return AjaxResult.success(orderService.addOrderReverseScan(scanPayRequest));
}
/**
*
*/
// @PreAuthorize("@ss.hasPermi('mall:order:list')")
@Anonymous
@GetMapping("/list")
public AjaxResult list(Order order) {
return AjaxResult.success( orderService.selectOrderList(order));
@PostMapping("/list")
public AjaxResult list(@RequestBody Order order) {
return AjaxResult.success(orderService.selectOrderList(order));
}
/**
*
*
*/
@Anonymous
@PostMapping("/addPay")
public AjaxResult addPay(@Validated @RequestBody CreateOrderDTO createOrderDTO) {
createOrderDTO.setCreateBy(getUsername());
return orderService.addPay(createOrderDTO);
}
/**
*
*
* <p>
*
* notifyUrl
* {"code":"success", "msg":"成功"}
* 10
* *
* {
* "code": "fail",
* "msg": "错误信息"
* }
*
*/
@PostMapping("/notify")
@Anonymous
public Map<String, String> notify(@RequestBody String callbackData) {
log.info("收到天阙平台支付回调, data:{}", callbackData);
Map<String, String> response = new HashMap<>();
try {
//解析回调数据
TianquePayCallbackDTO request = orderService.parseCallback(callbackData);
//检查业务编码0000表示成功
if (!request.isSuccess()) {
log.info("交易失败回调, ordNo:{}, bizCode:{}, bizMsg:{}",
request.getOrdNo(), request.getBizCode(), request.getBizMsg());
response.put("code", "fail");
response.put("msg", "交易失败");
return response;
}
//处理支付回调业务逻辑
orderService.handleTianquePayCallback(request);
//天阙平台要求的格式
response.put("code", "success");
response.put("msg", "成功");
log.info("天阙平台支付回调处理成功, ordNo:{}, uuid:{}",
request.getOrdNo(), request.getUuid());
} catch (ServiceException e) {
log.error("天阙平台支付回调业务处理失败: {}", e.getMessage());
response.put("code", "fail");
response.put("msg", e.getMessage());
} catch (Exception e) {
log.error("天阙平台支付回调处理异常", e);
response.put("code", "fail");
response.put("msg", "系统异常");
}
return response;
}
}

View File

@ -13,6 +13,7 @@ import com.ruoyi.common.utils.file.FileUploadUtils;
import com.ruoyi.common.utils.file.MimeTypeUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.web.domain.Product;
import com.ruoyi.web.dto.BatchBarcodeQueryDTO;
import com.ruoyi.web.dto.BatchStoreDTO;
import com.ruoyi.web.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
@ -33,6 +34,22 @@ public class ProductStoreController extends BaseController {
@Autowired
private ProductService productService;
/**
*
*
*/
@PostMapping("/getByBarcodes")
public AjaxResult getByBarcodes(@RequestBody BatchBarcodeQueryDTO dto) {
if (dto.getBarcodes() == null || dto.getBarcodes().isEmpty()) {
return AjaxResult.error("条码列表不能为空");
}
if (dto.getStoreId() == null) {
return AjaxResult.error("门店id不能为空");
}
return AjaxResult.success(productService.getByBarcodes(dto));
}
/**
*
*/
@ -77,7 +94,6 @@ public class ProductStoreController extends BaseController {
}
//todo 这里可以国码有的时候使用国码的图片路径
// TODO: 2026/1/14 这个file 为空的时候 就使用国码里面的图片,不为空代表用户自己选择了图片上传 用自己服务器上的
//国码后续添加
if (file != null) {

View File

@ -5,6 +5,9 @@ import lombok.Data;
import java.util.List;
/**
*
*/
@Data
public class Classification extends BaseEntity {
private static final long serialVersionUID = 1L;

View File

@ -0,0 +1,65 @@
package com.ruoyi.web.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import java.util.Date;
/**
* mall_device_info
*
*/
@Data
public class Device extends BaseEntity {
/** 设备ID */
private Long id;
/** 所属门店ID关联mall_store表的store_id */
private Long storeId;
/** 设备编号(厂商唯一标识) */
private String deviceCode;
/** 设备名称 */
private String deviceName;
/** 设备类型1-摄像头 2-扫码枪 3-打印机 4-收银机 */
private Integer deviceType;
/** 设备品牌 */
private String deviceBrand;
/** 设备型号 */
private String deviceModel;
/** 通道 */
private String channel;
/** MAC地址 */
private String macAddress;
/** 设备授权码/密钥(前端调设备用) */
private String authCode;
/** 安装日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
private Date installDate;
/** 有效期 */
@JsonFormat(pattern = "yyyy-MM-dd")
private Date expireDate;
/** 状态0-离线 1-在线 2-故障 3-已报废 */
private Integer deviceStatus;
/** 最后心跳时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date lastHeartbeat;
/** 备注 */
private String remark;
}

View File

@ -0,0 +1,30 @@
package com.ruoyi.web.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import java.util.Date;
/**
*
*/
@Data
public class DoorScanRecord extends BaseEntity {
private Long id;
private String operatorName; // 扫码人姓名
private String operatorPhone; // 扫码人手机号
private Integer storeId; // 门店ID
private String shopName; // 门店名称
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date scanTime; // 扫码时间
private String scanType; //'扫码类型 0:进门 1:出门'
}

View File

@ -0,0 +1,107 @@
package com.ruoyi.web.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import java.util.Date;
/**
* mall_install_task
*
* @author ruoyi
*/
@Data
public class InstallTask extends BaseEntity {
/** 任务ID */
private Long id;
/** 任务编号 */
private String taskNo;
/** 申请门店ID */
private Long storeId;
/** 门店名称 */
private String storeName;
/** 联系人 */
private String contactPerson;
/** 联系电话 */
private String contactPhone;
/** 设备类型1-摄像头 2-扫码枪 3-打印机 4-收银机 */
private Integer deviceType;
/** 期望设备型号 */
private String deviceModel;
/** 安装地址 */
private String installAddress;
/** 期望安装时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date expectedTime;
/** 备注/特殊要求 */
private String remark;
/** 状态0-待处理 1-已接单 2-安装中 3-已完成 4-已取消 */
private Integer taskStatus;
/** 指派人ID */
private Long assigneeId;
/** 指派人姓名 */
private String assigneeName;
/** 指派时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date assignTime;
/** 安装的设备ID */
private Long deviceId;
/** 设备编号 */
private String deviceCode;
/** 设备授权码 */
private String authCode;
/** 实际安装时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date installTime;
/** 安装照片URL */
private String installPhoto;
/** 完成备注 */
private String completeRemark;
/** 设备类型描述 */
public String getDeviceTypeDesc() {
if (deviceType == null) return "";
switch (deviceType) {
case 1: return "摄像头";
case 2: return "扫码枪";
case 3: return "打印机";
case 4: return "收银机";
default: return "未知";
}
}
/** 任务状态描述 */
public String getTaskStatusDesc() {
if (taskStatus == null) return "";
switch (taskStatus) {
case 0: return "待处理";
case 1: return "已接单";
case 2: return "安装中";
case 3: return "已完成";
case 4: return "已取消";
default: return "未知";
}
}
}

View File

@ -33,7 +33,7 @@ public class Order extends BaseEntity {
/**
*
*/
private BigDecimal payAmount;
private String payAmount;
/**
* : 0-,1-,2-,3-,4-退
@ -46,6 +46,11 @@ public class Order extends BaseEntity {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date payTime;
/**
*
*/
private String productBarCode;
/**
*
*/

View File

@ -5,6 +5,8 @@ import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
@ -25,9 +27,16 @@ public class OrderItem extends BaseEntity {
*/
private Long orderId;
/**
*
*/
private String productBarCode;
/**
* ID
*/
@NotNull(message = "商品ID不能为空")
private Long productId;
/**
@ -58,6 +67,8 @@ public class OrderItem extends BaseEntity {
/**
*
*/
@NotNull(message = "数量不能为空")
@Min(value = 1, message = "数量必须大于0")
private Integer quantity;
/**

View File

@ -85,16 +85,16 @@ public class Product extends BaseEntity {
/**
*
*/
private BigDecimal storePrice;
private String storePrice;
/**
*
*/
private BigDecimal costPrice;
private String costPrice;
/**
*
*/
private Long originalPrice;
private String originalPrice;
/**
* ID
@ -117,4 +117,15 @@ public class Product extends BaseEntity {
*/
private String status;
/**
*
*/
private Boolean isExpiring;
/**
*
*/
private Boolean isExpired;
}

View File

@ -0,0 +1,79 @@
package com.ruoyi.web.domain;
import lombok.Data;
/**
*
*/
@Data
public class TianquePayResponse {
/** 返回码0000-成功 */
private String code;
/** 返回信息 */
private String msg;
/** 签名 */
private String sign;
/** 签名类型 */
private String signType;
/** 组织机构ID */
private String orgId;
/** 请求ID */
private String reqId;
/** 响应数据 */
private RespData respData;
@Data
public static class RespData {
/** 业务码0000-成功 */
private String bizCode;
/** 业务信息 */
private String bizMsg;
/** 落单号 */
private String sxfUuid;
/** 商户订单号 */
private String ordNo;
/** 天阙平台订单号 */
private String uuid;
/** 预支付ID */
private String prepayId;
/** 支付AppID */
private String payAppId;
/** 时间戳 */
private String payTimeStamp;
/** 随机字符串 */
private String paynonceStr;
/** 支付包 */
private String payPackage;
/** 签名类型 */
private String paySignType;
/** 支付签名 */
private String paySign;
/** 商户号 */
private String partnerId;
/** 跳转地址 */
private String redirectUrl;
/** 来源 */
private String source;
}
}

View File

@ -0,0 +1,126 @@
package com.ruoyi.web.dto;
import java.io.Serializable;
public class ApiRequestBean<T> implements Serializable{
private static final long serialVersionUID = -9073910802005694017L;
/**
* id
*/
private String orgId;
/**
* id
*/
private String reqId;
/**
* 1.0
*/
private String version;
/**
*
*/
private String agentId;
/**
*
*/
private String salesCode;
/**
*
*/
private T reqData;
/**
*
*/
private String sign;
/**
*
*/
private String signType;
/**
*
*/
private String timestamp;
public String getOrgId() {
return orgId;
}
public void setOrgId(String orgId) {
this.orgId = orgId;
}
public String getReqId() {
return reqId;
}
public void setReqId(String reqId) {
this.reqId = reqId;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getAgentId() {
return agentId;
}
public void setAgentId(String agentId) {
this.agentId = agentId;
}
public String getSalesCode() {
return salesCode;
}
public void setSalesCode(String salesCode) {
this.salesCode = salesCode;
}
public T getReqData() {
return reqData;
}
public void setReqData(T reqData) {
this.reqData = reqData;
}
public String getSign() {
return sign;
}
public void setSign(String sign) {
this.sign = sign;
}
public String getSignType() {
return signType;
}
public void setSignType(String signType) {
this.signType = signType;
}
public String getTimestamp() {
return timestamp;
}
public void setTimestamp(String timestamp) {
this.timestamp = timestamp;
}
}

View File

@ -0,0 +1,15 @@
package com.ruoyi.web.dto;
import lombok.Data;
import java.util.List;
@Data
public class BatchBarcodeQueryDTO {
//商品条码
private List<String> barcodes;
// 可选门店ID用于过滤该门店的商品
private Long storeId;
}

View File

@ -0,0 +1,31 @@
package com.ruoyi.web.dto;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.web.domain.OrderItem;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;
@Data
public class CreateOrderDTO extends BaseEntity {
/** 门店ID */
@NotNull(message = "门店ID不能为空")
private Integer storeId;
/** 商品列表 */
@NotEmpty(message = "商品列表不能为空")
private List<OrderItem> items;
//总金额
@NotNull(message = "订单商品总金额不能为空")
private String totalAmount;
/** 用户openId */
private String openId;
/** 备注 */
private String remark;
}

View File

@ -0,0 +1,238 @@
package com.ruoyi.web.dto;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.math.BigDecimal;
import java.util.List;
@Data
public class ScanPayRequest extends BaseEntity {
/**
* 15
*/
private String mno;
/**
* 64//线
*/
private String ordNo;
/**
* ()#######.##
*/
private BigDecimal amt;
/**
* 64
* ///
*/
private String authCode;
/**
* scene36
* WECHAT:, ALIPAY:, UNIONPAY:, DCEP:, YZF:
*/
private String payType;
/**
* 1
* 1:, 3:, 6:线
*/
private String scene = "1";
/**
* 256
*/
private String subject;
/**
* ip16
*/
private String trmIp;
/**
* subAppId32
*/
private String subAppid;
/**
* 32
*
*/
private String wxGoodsTag;
/**
* 2
* 00:, 01:00
*/
private String goodsTag;
/**
* 2
* 00:, 01:
*/
private String needReceipt;
/**
* ()
*/
private BigDecimal discountAmt;
/**
* ()
*/
private BigDecimal unDiscountAmt;
/**
*
* 3:3, 6:6, 12:12
*/
private String hbFqNum;
/**
*
* 0:, 100:
*/
private String hbFqPercent;
/**
* 00
* 00:, 01:使
*/
private String limitPay = "00";
/**
* 1-14405
*/
private Integer timeExpire = 5;
/**
* 01
* 00:, 01:, 04:
*/
private String ledgerAccountFlag = "01";
/**
*
* ledgerAccountFlag0004
*/
private String ledgerAccountEffectTime;
/**
* ledgerAccountFlag
*/
private List<String> fusruleId;
/**
* 256
*
*/
private String notifyUrl;
/**
* 32
* :8:32
*/
private String ylTrmNo;
/**
*
* identityFlag=="00"IDCARD:
*/
private String buyerIdType;
/**
* 18
* identityFlag=="00"
*/
private String buyerIdNo;
/**
* 32
* identityFlag=="00"
*/
private String buyerName;
/**
* 11
* identityFlag=="00"
*/
private String mobileNum;
/**
* 32store_id
*/
private String storeId;
/**
*
*/
private String alipayStoreId;
/**
* 128
*
*/
private String deviceNo;
/**
* 32
*/
private String storeNum;
/**
*
*/
private String senceNo;
/**
* 10
*/
private String encryptRandNum;
/**
* 16
*/
private String secretText;
/**
* 16
* +-西
*/
private String longitude;
/**
* 16
* +-
*/
private String latitude;
/**
* ID20
*/
private String eduSchoolId;
/**
* 30
*/
private String eduScene;
/**
*
* business_params
*/
private String zfbBusinessParams;
/**
* 128
*/
private String extend;
}

View File

@ -0,0 +1,171 @@
package com.ruoyi.web.dto;
import lombok.Data;
import java.math.BigDecimal;
/**
*
*
*/
@Data
public class TianquePayCallbackDTO {
/** 响应时间戳 */
private String timeStamp;
/** 业务数据签名结果 */
private String sign;
/** 商户编号 */
private String mno;
/** 商户订单号 */
private String ordNo;
/** 天阙平台订单号 */
private String uuid;
/** 交易支付完成时间格式YYYYMMDDHHmmss */
private String payTime;
/** 订单总金额(元),格式:#######.## */
private String amt;
/** 支付渠道 */
private String payType;
/** 交易方式00 主扫 01 被扫 02 公众号 03 小程序 */
private String payWay;
/** 终端号 */
private String ylTrmNo;
/** 原商户订单号(退款通知返回) */
private String origOrdNo;
/** 原天阙订单号(退款通知返回) */
private String origUuid;
/** 支付场景1 付款码 2 声波 3 刷脸 */
private String scene;
/** 买家用户号 */
private String buyerId;
/** 支付宝买家登录账号 */
private String buyerAccount;
/** 微信/支付宝流水号 */
private String transactionId;
/** 借贷标识1 借记卡 2 贷记卡 3 其他 */
private String drType;
/** 消费者付款金额 */
private String totalOffstAmt;
/** 商家入账金额 */
private String settleAmt;
/** 付款银行 */
private String payBank;
/** 代金券金额 */
private String pointAmount;
/** 交易手续费率 */
private String recFeeRate;
/** 交易手续费 */
private String recFeeAmt;
/** 商家出款金额 */
private String realRefundAmount;
/** 渠道商商户号 */
private String channelId;
/** 子商户号 */
private String subMechId;
/** 消费者到账金额 */
private String refBuyerAmt;
/** 微信或支付宝的身份ID */
private String openid;
/** 活动类型 */
private String activityNo;
/** 落单号 */
private String sxfUuid;
/** 扩展字段 */
private String extend;
/** 优惠信息 */
private String promotionDetail;
/** 天阙门店编号 */
private String storeNum;
/** 清算日期 */
private String clearDt;
/** 交易完成时间 */
private String finishTime;
/** 天阙终端编号 */
private String deviceNo;
/** 原交易金额 */
private String origAmt;
/** 原交易订单号 */
private String origOrderNo;
/** 优惠券退款金额 */
private String couponRefundFee;
/** 是否收支两条线标识 */
private String szltFlag;
/** 后收手续费 */
private String szltRecfeeAmt;
/** 切批时间 */
private String settlementBatchNo;
/** 支付宝异步刷脸支付参数 */
private String asyncPaymentMode;
/** 分期信息 */
private String hbFqPayInfo;
/** 业务编码 */
private String bizCode;
/** 业务信息 */
private String bizMsg;
/** 组织机构ID */
private String orgId;
/** 交易类型 */
private String tradeType;
/** 交易信息 */
private String tradeMsg;
/** 原始回调数据 */
private String rawData;
/**
*
*/
public boolean isSuccess() {
return "0000".equals(bizCode);
}
}

View File

@ -0,0 +1,12 @@
package com.ruoyi.web.dto;
import lombok.Data;
@Data
public class TradeQueryDTO {
/**
*
*/
private String ordNo;
}

View File

@ -0,0 +1,17 @@
package com.ruoyi.web.mapper;
import com.ruoyi.web.domain.Device;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface DeviceMapper {
List<Device> getDevice(@Param("storeId") String storeId);
int addDevice(Device device);
List<Device> getSheXiangTou(String storeId);
}

View File

@ -0,0 +1,20 @@
package com.ruoyi.web.mapper;
import com.ruoyi.web.domain.DoorScanRecord;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface DoorScanRecordMapper {
int addDoorScanRecord(DoorScanRecord doorScanRecord);
List<DoorScanRecord> selectScanRecordList(DoorScanRecord doorScanRecord);
Integer getEnterCountByDate(@Param("date") String date,@Param("storeId") Integer storeId);
}

View File

@ -0,0 +1,61 @@
package com.ruoyi.web.mapper;
import com.ruoyi.web.domain.InstallTask;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* Mapper
*/
public interface InstallTaskMapper {
/**
*
*/
int insertInstallTask(InstallTask installTask);
/**
* ID
*/
InstallTask selectInstallTaskById(@Param("id") Long id);
/**
*
*/
InstallTask selectInstallTaskByTaskNo(@Param("taskNo") String taskNo);
/**
*
*/
List<InstallTask> selectInstallTaskList(InstallTask installTask);
/**
*
*/
int updateInstallTask(InstallTask installTask);
/**
*
*/
int deleteInstallTaskById(@Param("id") Long id);
/**
*
*/
int deleteInstallTaskByIds(@Param("ids") Long[] ids);
/**
*
*/
int assignTask(InstallTask installTask);
/**
*
*/
int completeTask(InstallTask installTask);
/**
*
*/
String selectMaxTaskNoByDate(@Param("prefix") String prefix);
}

View File

@ -2,7 +2,10 @@ package com.ruoyi.web.mapper;
import com.ruoyi.web.domain.Order;
import com.ruoyi.web.domain.OrderItem;
import com.ruoyi.web.vo.OrderCountVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@ -10,7 +13,22 @@ import java.util.List;
@Mapper
public interface OrderMapper {
List<Order> selectOrderList(Order order);
int insert(Order order);
int batchInsert(@Param("list") List<OrderItem> items);
Order selectForUpdateByOrderNo(@Param("ordNo") String ordNo);
List<OrderItem> selectByOrderId(@Param("orderId") Long orderId);
/**
* ID
*/
int updateById(Order order);
OrderCountVO getOrderCount(String storeId);
}

View File

@ -2,6 +2,8 @@ package com.ruoyi.web.mapper;
import com.ruoyi.web.domain.Product;
import com.ruoyi.web.domain.Store;
import com.ruoyi.web.dto.BatchBarcodeQueryDTO;
import com.ruoyi.web.vo.ProductVo;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@ -26,4 +28,29 @@ public interface ProductMapper {
int batchProductById(@Param("ids") List<Long> ids);
List<Product> selectBatchForUpdate(@Param("storeId") Integer storeId,
@Param("ids") List<Long> ids);
/**
* ID
* @param storeId ID
* @param productId ID
* @param quantity
* @return >0=0
*/
int reduceStock(@Param("storeId") Integer storeId,
@Param("productId") Long productId,
@Param("quantity") Integer quantity);
/**
* ID
*/
int addStock(@Param("storeId") Integer storeId,
@Param("productId") Long productId,
@Param("quantity") Integer quantity);
List<Product> getByBarcodes(BatchBarcodeQueryDTO dto);
}

View File

@ -2,8 +2,6 @@ package com.ruoyi.web.mapper;
import com.ruoyi.web.domain.Store;
import com.ruoyi.web.vo.StoreVo;
import com.ruoyi.web.vo.UserStoreVo;
import org.apache.ibatis.annotations.Param;
import java.util.List;

View File

@ -0,0 +1,15 @@
package com.ruoyi.web.service;
import com.ruoyi.web.domain.Brand;
import com.ruoyi.web.domain.Device;
import java.util.List;
public interface DeviceService {
List<Device> getDevice(String storeId);
int addDevice(Device device);
List<Device> getSheXiangTou(String storeId);
}

View File

@ -0,0 +1,15 @@
package com.ruoyi.web.service;
import com.ruoyi.web.domain.DoorScanRecord;
import java.util.List;
public interface DoorScanRecordService {
int addDoorScanRecord(DoorScanRecord doorScanRecord);
List<DoorScanRecord> selectScanRecordList(DoorScanRecord doorScanRecord);
Integer getEnterCountByDate(String date,Integer storeId);
}

View File

@ -0,0 +1,61 @@
package com.ruoyi.web.service;
import com.ruoyi.web.domain.InstallTask;
import java.util.List;
/**
* Service
*/
public interface InstallTaskService {
/**
*
*/
int insertInstallTask(InstallTask installTask);
/**
* ID
*/
InstallTask selectInstallTaskById(Long id);
/**
*
*/
InstallTask selectInstallTaskByTaskNo(String taskNo);
/**
*
*/
List<InstallTask> selectInstallTaskList(InstallTask installTask);
/**
*
*/
int updateInstallTask(InstallTask installTask);
/**
*
*/
int deleteInstallTaskById(Long id);
/**
*
*/
int deleteInstallTaskByIds(Long[] ids);
/**
*
*/
int assignTask(Long id, Long assigneeId, String assigneeName, String updateBy);
/**
*
*/
int completeTask(InstallTask installTask);
/**
*
*/
String generateTaskNo();
}

View File

@ -1,7 +1,13 @@
package com.ruoyi.web.service;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.web.domain.Order;
import com.ruoyi.web.dto.CreateOrderDTO;
import com.ruoyi.web.dto.ScanPayRequest;
import com.ruoyi.web.dto.TianquePayCallbackDTO;
import com.ruoyi.web.dto.TradeQueryDTO;
import com.ruoyi.web.vo.OrderCountVO;
import java.util.List;
@ -13,4 +19,21 @@ public interface OrderService {
List<Order> selectOrderList(Order order);
AjaxResult addPay(CreateOrderDTO createOrderDTO);
TianquePayCallbackDTO parseCallback(String callbackData);
/**
*
*/
void handleTianquePayCallback(TianquePayCallbackDTO request);
AjaxResult addOrder(CreateOrderDTO createOrderDTO);
String addOrderReverseScan(ScanPayRequest scanPayRequest) throws Exception;
AjaxResult tradeQuery(TradeQueryDTO tradeQueryDTO) throws Exception;
OrderCountVO getOrderCount(String storeId);
}

View File

@ -1,7 +1,9 @@
package com.ruoyi.web.service;
import com.ruoyi.web.domain.Product;
import com.ruoyi.web.dto.BatchBarcodeQueryDTO;
import com.ruoyi.web.dto.BatchStoreDTO;
import com.ruoyi.web.vo.ProductVo;
import java.util.List;
@ -35,4 +37,6 @@ public interface ProductService {
int updateProduct(Product product);
int batchProductById(BatchStoreDTO dto);
List<Product> getByBarcodes(BatchBarcodeQueryDTO dto);
}

View File

@ -0,0 +1,32 @@
package com.ruoyi.web.service.impl;
import com.ruoyi.web.domain.Device;
import com.ruoyi.web.mapper.DeviceMapper;
import com.ruoyi.web.service.DeviceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class DeviceServiceImpl implements DeviceService {
@Autowired
private DeviceMapper deviceMapper;
@Override
public List<Device> getDevice(String storeId) {
return deviceMapper.getDevice(storeId);
}
@Override
public int addDevice(Device device) {
return deviceMapper.addDevice(device);
}
@Override
public List<Device> getSheXiangTou(String storeId) {
return deviceMapper.getSheXiangTou(storeId);
}
}

View File

@ -0,0 +1,31 @@
package com.ruoyi.web.service.impl;
import com.ruoyi.web.domain.DoorScanRecord;
import com.ruoyi.web.mapper.DoorScanRecordMapper;
import com.ruoyi.web.service.DoorScanRecordService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class DoorScanRecordServiceImpl implements DoorScanRecordService {
@Autowired
private DoorScanRecordMapper doorScanRecordMapper;
@Override
public int addDoorScanRecord(DoorScanRecord doorScanRecord) {
return doorScanRecordMapper.addDoorScanRecord(doorScanRecord);
}
@Override
public List<DoorScanRecord> selectScanRecordList(DoorScanRecord doorScanRecord) {
return doorScanRecordMapper.selectScanRecordList(doorScanRecord);
}
@Override
public Integer getEnterCountByDate(String date,Integer storeId) {
return doorScanRecordMapper.getEnterCountByDate(date,storeId);
}
}

View File

@ -0,0 +1,119 @@
package com.ruoyi.web.service.impl;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.web.domain.InstallTask;
import com.ruoyi.web.mapper.InstallTaskMapper;
import com.ruoyi.web.service.InstallTaskService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
/**
* Service
*/
@Service
public class InstallTaskServiceImpl implements InstallTaskService {
@Resource
private InstallTaskMapper installTaskMapper;
@Override
public int insertInstallTask(InstallTask installTask) {
// 生成任务编号
installTask.setTaskNo(generateTaskNo());
// 设置初始状态为待处理
installTask.setTaskStatus(0);
// 设置创建时间
installTask.setCreateTime(DateUtils.getNowDate());
return installTaskMapper.insertInstallTask(installTask);
}
@Override
public InstallTask selectInstallTaskById(Long id) {
return installTaskMapper.selectInstallTaskById(id);
}
@Override
public InstallTask selectInstallTaskByTaskNo(String taskNo) {
return installTaskMapper.selectInstallTaskByTaskNo(taskNo);
}
@Override
public List<InstallTask> selectInstallTaskList(InstallTask installTask) {
return installTaskMapper.selectInstallTaskList(installTask);
}
@Override
public int updateInstallTask(InstallTask installTask) {
installTask.setUpdateTime(DateUtils.getNowDate());
return installTaskMapper.updateInstallTask(installTask);
}
@Override
public int deleteInstallTaskById(Long id) {
return installTaskMapper.deleteInstallTaskById(id);
}
@Override
public int deleteInstallTaskByIds(Long[] ids) {
return installTaskMapper.deleteInstallTaskByIds(ids);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int assignTask(Long id, Long assigneeId, String assigneeName, String updateBy) {
InstallTask task = new InstallTask();
task.setId(id);
task.setAssigneeId(assigneeId);
task.setAssigneeName(assigneeName);
task.setTaskStatus(1); // 1-已接单
task.setAssignTime(DateUtils.getNowDate());
task.setUpdateBy(updateBy);
task.setUpdateTime(DateUtils.getNowDate());
return installTaskMapper.assignTask(task);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int completeTask(InstallTask installTask) {
installTask.setTaskStatus(3); // 3-已完成
installTask.setInstallTime(DateUtils.getNowDate());
installTask.setUpdateTime(DateUtils.getNowDate());
return installTaskMapper.completeTask(installTask);
}
@Override
public String generateTaskNo() {
// 格式INS + 年月日 + 4位序号
// 例如INS202503160001
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String today = sdf.format(new Date());
String prefix = "INS" + today;
// 查询当天最大的任务编号
String maxTaskNo = installTaskMapper.selectMaxTaskNoByDate(prefix);
int seq = 1;
if (maxTaskNo != null && maxTaskNo.length() >= 14) {
// 截取后4位序号
String seqStr = maxTaskNo.substring(11);
try {
seq = Integer.parseInt(seqStr) + 1;
} catch (NumberFormatException e) {
seq = 1;
}
}
// 生成4位序号不足补0
String seqStr = String.format("%04d", seq);
return prefix + seqStr;
}
}

View File

@ -1,26 +1,803 @@
package com.ruoyi.web.service.impl;
import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.web.domain.Order;
import com.ruoyi.web.domain.OrderItem;
import com.ruoyi.web.domain.Product;
import com.ruoyi.web.dto.*;
import com.ruoyi.web.mapper.OrderMapper;
import com.ruoyi.web.mapper.ProductMapper;
import com.ruoyi.web.service.OrderService;
import com.ruoyi.web.utils.HttpUtils;
import com.ruoyi.web.utils.RSASignature;
import com.ruoyi.web.vo.OrderCountVO;
import com.ruoyi.web.vo.OrderVo;
import com.ruoyi.web.vo.PaymentResponseVO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;
import javax.annotation.Resource;
import java.util.List;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
/**
*
*/
@Service
public class OrderServiceImpl implements OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderServiceImpl.class);
@Resource
private OrderMapper orderMapper;
@Resource
private ProductMapper productMapper;
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String ORDER_LOCK_PREFIX = "order:lock:";
@Override
public List<Order> selectOrderList(Order order) {
return orderMapper.selectOrderList(order);
}
@Override
@Transactional(rollbackFor = Exception.class)
public AjaxResult addPay(CreateOrderDTO createOrderDTO) {
//订单号
String orderNo = null;
//订单id
Long orderId = null;
//锁key
String lockKey = null;
//订单明细
List<OrderItem> orderItems = new ArrayList<>();
//金额
BigDecimal totalAmount = BigDecimal.ZERO;
//支付控件参数
String pay = null;
try {
// 1. 生成锁key基于门店ID + 商品组合)
lockKey = generateLockKey(createOrderDTO.getStoreId(), createOrderDTO.getItems());
log.info("尝试获取锁: {}", lockKey);
// 2. 获取分布式锁
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", Duration.ofSeconds(6));
if (Boolean.FALSE.equals(locked)) {
log.warn("获取锁失败,该门店的商品组合正在被处理: {}", lockKey);
throw new ServiceException("系统繁忙,请稍后重试");
}
try {
// 3. 批量查询商品并加行锁带上门店ID条件
List<Long> productIds = createOrderDTO.getItems().stream()
.map(OrderItem::getProductId)
.collect(Collectors.toList());
List<Product> products = productMapper.selectBatchForUpdate(
createOrderDTO.getStoreId(), productIds);
if (products.size() != productIds.size()) {
// 找出不存在的商品
Set<Long> existIds = products.stream()
.map(Product::getId)
.collect(Collectors.toSet());
List<Long> notExistIds = productIds.stream()
.filter(id -> !existIds.contains(id))
.collect(Collectors.toList());
log.error("部分商品在该门店不存在, 门店id storeId:{}, 商品id productIds:{}",
createOrderDTO.getStoreId(), notExistIds);
// return AjaxResult.error("部分商品在该门店不存在");
throw new ServiceException("部分商品在该门店不存在");
}
Map<Long, Product> productMap = products.stream()
.collect(Collectors.toMap(Product::getId, p -> p));
// 5. 校验库存并计算金额
for (OrderItem item : createOrderDTO.getItems()) {
Product product = productMap.get(item.getProductId());
if (product.getStockQuantity() < item.getQuantity()) {
log.error("商品[" + product.getProductName() + "]库存不足,当前门店剩余库存数量:" + product.getStockQuantity());
return AjaxResult.error("商品[" + product.getProductName() + "]库存不足,当前门店剩余库存数量:" + product.getStockQuantity());
}
// 计算小计
BigDecimal storePrice = new BigDecimal(product.getStorePrice());
BigDecimal subtotal = storePrice
.multiply(new BigDecimal(item.getQuantity()))
.setScale(2, RoundingMode.HALF_UP);
totalAmount = totalAmount.add(subtotal);
// 创建订单明细
OrderItem orderItem = new OrderItem();
orderItem.setProductId(product.getId());
orderItem.setProductName(product.getProductName());
orderItem.setMainImage(product.getMainImage());
orderItem.setQuantity(item.getQuantity());
orderItem.setSubtotal(item.getSubtotal());
orderItem.setCreateBy(createOrderDTO.getCreateBy());
orderItems.add(orderItem);
}
// 6. 校验前端传递的金额和后端计算的金额是否一致
BigDecimal frontendAmount = new BigDecimal(createOrderDTO.getTotalAmount());
if (frontendAmount.compareTo(totalAmount) != 0) {
log.error("金额不一致,前端传参金额:{},后端计算金额:{}",
frontendAmount, totalAmount);
return AjaxResult.error("订单总金额异常");
}
// 7. 生成订单号
orderNo = generateOrderNo();
// 8. 创建订单主表
Order order = new Order();
order.setOrderNo(orderNo);
order.setStoreId(createOrderDTO.getStoreId());
order.setPayAmount(String.valueOf(totalAmount));
order.setPayStatus(0); // 未支付
order.setCreateBy(createOrderDTO.getCreateBy());
int insert = orderMapper.insert(order);
if (insert <= 0) {
log.error("主订单创建失败");
throw new ServiceException("主订单创建失败");
}
orderId = order.getId();
// 9. 批量插入订单明细
for (OrderItem item : orderItems) {
item.setOrderId(orderId);
}
int batchInsertResult = orderMapper.batchInsert(orderItems);
if (batchInsertResult != orderItems.size()) {
log.error("订单明细创建失败");
throw new ServiceException("订单明细创建失败");
}
} finally {
// 释放锁
redisTemplate.delete(lockKey);
log.info("释放锁: {}", lockKey);
}
// ============ 锁已释放,开始调用支付接口 ============
pay = pay(orderNo, String.valueOf(totalAmount)); //调用第三方支付成功,返回调用支付控件参数
} catch (Exception e) {
log.error("创建订单失败", e);
throw new ServiceException("创建订单失败" + e.getMessage());
}
log.info("订单创建成功, orderNo:{}, 订单ID:{}, storeId:{},支付控件参数:{}",
orderNo, orderId, createOrderDTO.getStoreId(), pay);
return AjaxResult.success("订单创建成功: " + pay);
}
@Override
public TianquePayCallbackDTO parseCallback(String callbackData) {
try {
return JSON.parseObject(callbackData, TianquePayCallbackDTO.class);
} catch (Exception e) {
log.error("解析天阙平台回调数据失败", e);
throw new RuntimeException("解析回调数据失败");
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void handleTianquePayCallback(TianquePayCallbackDTO request) {
log.info("处理天阙平台支付回调, ordNo:{}, uuid:{}, amt:{}",
request.getOrdNo(), request.getUuid(), request.getAmt());
// 1. 参数校验
if (request.getOrdNo() == null || request.getUuid() == null) {
log.error("回调参数缺失");
throw new ServiceException("回调参数缺失");
}
// 2. 幂等处理(防止重复回调)
String idempotentKey = "tianque:callback:" + request.getUuid();
Boolean isFirst = redisTemplate.opsForValue()
.setIfAbsent(idempotentKey, "1", Duration.ofHours(24));
if (Boolean.FALSE.equals(isFirst)) {
log.info("天阙平台回调已处理过, uuid:{}", request.getUuid());
throw new ServiceException("天阙平台回调已处理过");
}
// 3. 获取订单锁(防止并发处理同一个订单)
String orderLockKey = ORDER_LOCK_PREFIX + request.getOrdNo();
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(orderLockKey, "1", Duration.ofSeconds(5));
if (Boolean.FALSE.equals(locked)) {
log.warn("订单正在处理中, ordNo:{}", request.getOrdNo());
throw new ServiceException("订单正在处理中");
}
try {
// 4. 查询订单(加行锁)
Order order = orderMapper.selectForUpdateByOrderNo(request.getOrdNo());
if (order == null) {
log.error("订单不存在, orderNo:{}", request.getOrdNo());
throw new ServiceException("订单不存在");
}
// 5. 检查订单状态
if (order.getPayStatus() == 2) {
log.info("订单已支付, orderNo:{}", request.getOrdNo());
throw new ServiceException("订单已支付");
}
if (order.getPayStatus() != 0) {
log.error("订单状态错误, orderNo:{}, status:{}",
request.getOrdNo(), order.getPayStatus());
throw new ServiceException("订单状态错误");
}
// 6. 金额校验
if (order.getPayAmount().compareTo(request.getAmt()) != 0) {
log.error("支付金额不匹配, orderAmount:{}, payAmount:{}",
order.getPayAmount(), request.getAmt());
throw new ServiceException("支付金额不匹配");
}
// 7. 查询订单明细
List<OrderItem> items = orderMapper.selectByOrderId(order.getId());
// 8. 扣减实际库存
for (OrderItem item : items) {
int result = productMapper.reduceStock(
order.getStoreId(),
item.getProductId(),
item.getQuantity()
);
if (result <= 0) {
log.error("库存扣减失败, storeId:{}, productId:{}, quantity:{}",
order.getStoreId(), item.getProductId(), item.getQuantity());
throw new ServiceException("库存扣减失败");
}
}
// 9. 更新订单状态为已支付
Order updateOrder = new Order();
updateOrder.setId(order.getId());
updateOrder.setPayStatus(2);
updateOrder.setPayTime(new Date());
int updated = orderMapper.updateById(updateOrder);
if (updated <= 0) {
log.error("订单状态更新失败, orderNo:{}", request.getOrdNo());
throw new ServiceException("订单状态更新失败");
}
log.info("支付回调处理成功, orderNo:{}, storeId:{}",
request.getOrdNo(), order.getStoreId());
} finally {
// 释放订单锁
redisTemplate.delete(orderLockKey);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public AjaxResult addOrder(CreateOrderDTO createOrderDTO) {
String orderNo = null;
BigDecimal totalAmount = BigDecimal.ZERO;
//锁key
String lockKey = null;
orderNo = generateOrderNo();
try {
// 1. 生成锁key基于门店ID + 商品组合)
lockKey = generateLockKey(createOrderDTO.getStoreId(), createOrderDTO.getItems());
log.info("尝试获取锁: {}", lockKey);
// 2. 获取分布式锁
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", Duration.ofSeconds(6));
if (Boolean.FALSE.equals(locked)) {
log.warn("获取锁失败,该门店的商品组合正在被处理: {}", lockKey);
throw new ServiceException("系统繁忙,请稍后重试");
}
try {
// 3. 批量查询商品并加行锁带上门店ID条件
List<Long> productIds = createOrderDTO.getItems().stream()
.map(OrderItem::getProductId)
.collect(Collectors.toList());
List<Product> products = productMapper.selectBatchForUpdate(
createOrderDTO.getStoreId(), productIds);
if (products.size() != productIds.size()) {
// 找出不存在的商品
Set<Long> existIds = products.stream()
.map(Product::getId)
.collect(Collectors.toSet());
List<Long> notExistIds = productIds.stream()
.filter(id -> !existIds.contains(id))
.collect(Collectors.toList());
log.error("部分商品在该门店不存在, 门店id storeId:{}, 商品id productIds:{}",
createOrderDTO.getStoreId(), notExistIds);
throw new ServiceException("部分商品在该门店不存在");
}
Map<Long, Product> productMap = products.stream()
.collect(Collectors.toMap(Product::getId, p -> p));
// 5. 校验库存并计算金额
for (OrderItem item : createOrderDTO.getItems()) {
Product product = productMap.get(item.getProductId());
if (product.getStockQuantity() < item.getQuantity()) {
log.error("商品[" + product.getProductName() + "]库存不足,当前门店剩余库存数量:" + product.getStockQuantity());
return AjaxResult.error("商品[" + product.getProductName() + "]库存不足,当前门店剩余库存数量:" + product.getStockQuantity());
}
// 计算小计
BigDecimal storePrice = new BigDecimal(product.getStorePrice());
BigDecimal subtotal = storePrice
.multiply(new BigDecimal(item.getQuantity()))
.setScale(2, RoundingMode.HALF_UP);
totalAmount = totalAmount.add(subtotal);
}
// 6. 校验前端传递的金额和后端计算的金额是否一致
BigDecimal frontendAmount = new BigDecimal(createOrderDTO.getTotalAmount());
if (frontendAmount.compareTo(totalAmount) != 0) {
log.error("金额不一致,前端传参金额:{},后端计算金额:{}",
frontendAmount, totalAmount);
return AjaxResult.error("订单总金额异常");
}
//订单id
Long orderId = null;
Order order = new Order();
order.setOrderNo(orderNo);
order.setStoreId(createOrderDTO.getStoreId());
order.setPayAmount(String.valueOf(createOrderDTO.getTotalAmount()));
order.setPayStatus(0); // 未支付
order.setCreateBy(createOrderDTO.getCreateBy());
int insert = orderMapper.insert(order);
if (insert <= 0) {
log.error("主订单创建失败");
throw new ServiceException("主订单创建失败");
}
orderId = order.getId();
for (OrderItem item : createOrderDTO.getItems()) {
item.setOrderId(orderId);
}
orderMapper.batchInsert(createOrderDTO.getItems());
log.info("被扫订单创建成功");
} finally {
// 释放锁
redisTemplate.delete(lockKey);
log.info("释放锁: {}", lockKey);
}
} catch (Exception e) {
log.error("创建订单失败", e);
throw new ServiceException("创建订单失败" + e.getMessage());
}
OrderVo orderVo = new OrderVo();
orderVo.setOrderNo(orderNo);
orderVo.setAmount(String.valueOf(totalAmount));
return AjaxResult.success(orderVo);
}
@Override
@Transactional(rollbackFor = Exception.class)
public String addOrderReverseScan(ScanPayRequest scanPayRequest) throws Exception {
//再加个校验,根据订单号去查询金额,如果前端传的金额和后端存的不一样,不允许支付
//合作方私钥(替换成自己的)
String privateKey = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDM35qbWVNdBAU+oai8EVE3iSpA9wHAt4wOs+SufbkE/FNoMgp4EMrFNrdNkaKLQq4cRzqoNlJY+DZSL54dm7ekKZP/SLhWwNi9tJvhKsFpw6SRwthlKyA8lCW2mzFffFqLhGAayNoZTmHwkq8dotvecARVbiFtakzzFy/O7ATmzw3i57yaVgX2RtD8WWA7MGg5CQP1vvIDlXqd40SQcrJY6GNNv07Brv3fpyW2nXbvrxbOx+87NnG77/R7o8eFcWJF+yrqE1xeN3hDvHBFMFyf1M39Zig2I75vKRvHGqftAZuvlDG0nInwcSiMTE7wDsXvYzRnOsgJNVET7aFhzOvFAgMBAAECggEATjB4nRl2S2Whasnf0Ab77CoZSjn7HUXv2hymBkJNPq3eV0Hh5Pjjqmi7hIs0cdm9AWXd6RzySKpScQNjYQFxEOIdbayfTdzD24L2ivT6ZBca09Z9J/9Rik0wJ4ULTtny5JRc6VwGgOn9Twdsde8V4sr1nHUvpMaRH6WVxW8mX7+udgbK0Uxm5IFROeKGz0DSuG8SBpDhD2EB6P2Q+1fpna17yP1R08HtZNU6lQx7yMesaJuvKkwf4YiSjIhUtdbUXGhmbG5LFzoj3oRs25W68bIOQjT9i196hPQOYj6C7yz72nuY2rwFHhtsocfQRULWOHtsnWts9epvuFruDTdq8QKBgQD8bZhQkyQm6VTP0Hmv6sV9SXXKH0rb2V8hml7DxcpFl+l2/9UesH3qMoFKZ5C1YdPKZBNlD7fp5RxjtJ8MdwIp8MxRgMf/bZpIOx1Lfum1H+dKh5LF+oC4GGtJ5jqnwozNPog4keUn+bqKhiza9snA+6jy446A1Y8GGWlZwUGkVwKBgQDPxb7FZmOcotNnh8mS5pcx6d22UhBHIHz/X/dcjNQuI8tIRaIvkJm/4lfuxejCz3LY9k+VSQ96ZM2L5OqZGDlR7aMXQ3/FhuS4PEkNcDvccQPoig7U5zUUn2Jdqom6Lngh7Ka8B1v24ght0yXHHNChmMg/DJ42EzCnjpydWs6/QwKBgA9qGd0BvzlpEjbGgkfNzFWEQN8g3g9izL5ekN7fmyR4zFbp9He1S1sbzm1euaV53dcEGXMYbKCpzvv/sZ6vPcCV5cQsWwosBTnX8kgD7f2Tfyo78SiJzYZwZ0zR9E7+QF7gLK1Xq2ivhUakPuT4IQXZ2E1MvAz9/Yff0WEbvghtAoGAejFzC8c2yDUenaHHU+TXgNxor0Q+HIan3M0EvmJ4mxYkBMInK1AgjDBCxMOSK5gzlBPwI/0O5E2KcT7VFeqgM2XN5+2jpHi75PpXgFbEbdXtlYI0TNQZbKJ8CFg2nc+ciV8ThDvTwzOV/3kRm7N/o7ol8qaqGWVZ1QFTbFuugd8CgYBAsNVKCEHGZAwS5Csv/IBopcrJOJknmEHeAT+VwxtZK7yOJ4ZSNizzofu847YGO4uEhfcaC8lYxnj8XskuMG2KChbbxEA+qlnL/oMIwVrpE0ZbFmQ4WRTUfVQT+zCHMPuuuj5tLPTSMjQLGlq+r3XRzwUaX98J9GLKgHG7tXlphw==";
//随行付公钥
String sxfPublic = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjo1+KBcvwDSIo+nMYLeOJ19Ju4ii0xH66ZxFd869EWFWk/EJa3xIA2+4qGf/Ic7m7zi/NHuCnfUtUDmUdP0JfaZiYwn+1Ek7tYAOc1+1GxhzcexSJLyJlR2JLMfEM+rZooW4Ei7q3a8jdTWUNoak/bVPXnLEVLrbIguXABERQ0Ze0X9Fs0y/zkQFg8UjxUN88g2CRfMC6LldHm7UBo+d+WlpOYH7u0OTzoLLiP/04N1cfTgjjtqTBI7qkOGxYs6aBZHG1DJ6WdP+5w+ho91sBTVajsCxAaMoExWQM2ipf/1qGdsWmkZScPflBqg7m0olOD87ymAVP/3Tcbvi34bDfwIDAQAB";
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
String dataStr = df.format(new Date());
ApiRequestBean<JSONObject> reqBean = new ApiRequestBean<JSONObject>();
reqBean.setOrgId("57594783"); //机构id
reqBean.setReqId(UUID.randomUUID().toString().replaceAll("-", ""));
reqBean.setSignType("RSA");
reqBean.setTimestamp(dataStr);
reqBean.setVersion("1.0");
JSONObject reqData = new JSONObject();
//业务参数
reqData.put("ordNo", scanPayRequest.getOrdNo()); //商户订单号
reqData.put("mno", "399260311280126"); //商户编号
//reqData.put("subMechId", ""); //子商户号
reqData.put("authCode", scanPayRequest.getAuthCode()); //授权码 需要前端传
reqData.put("amt", scanPayRequest.getAmt()); //订单总金额
//reqData.put("discountAmt", ""); //参与优惠金额
//reqData.put("unDiscountAmt", ""); //不参与优惠金额
reqData.put("payType", scanPayRequest.getPayType()); //支付渠道
reqData.put("scene", "1"); //支付场景1 刷卡 2声波 3刷脸 不上传默认为 1
reqData.put("subject", "B扫C测试"); //订单标题
reqData.put("tradeSource", "02"); //交易来源 01服务商02收银台03硬件
reqData.put("trmIp", "172.16.2.1");
//reqData.put("limitPay", "00"); //限制卡类型: 00-全部 01-限定不能使 用信用卡支付 默认值 00
//reqData.put("hbFqNum", "6"); //花呗分期数,仅可上送 6 或 12
//reqData.put("hbFqPercent", "0"); //卖家承担分期 服务费比例,仅支持上送 0 或 100
//reqData.put("goodsTag", "00"); //订单优惠标识 0001
//reqData.put("couponDetail", ""); //优惠详情信息,见下面三个字段
//reqData.put("costPrice", "200"); //订单原价保留两 位小数;微信 独有
//reqData.put("receiptId ", "123456789"); //商品小票
//reqData.put("goodsDetail", "123456789"); //单品优惠信息使用 json 数组格式提交
//reqData.put("goodsId", "200"); //商品编码
//reqData.put("thirdGoodsId", "12345678"); //微信/支付宝侧商品码
//reqData.put("goodsName", "苹果电脑"); //商品名称
//reqData.put("quantity", "1"); //商品数量
//reqData.put("price", "1.01"); //商品单价
//reqData.put("goodsCategory", ""); //商品类目;支 付宝独有
//reqData.put("categoriesTree", "124868003|126232002|126252004"); //商品类目树
//reqData.put("goodsDesc", ""); //商品描述;支 付宝独有
//reqData.put("showUrl", ""); //商品展示地址 url支付宝独有
//reqData.put("needReceipt", "00"); //电子发票功能 微信开具电子 发票使用
//reqData.put("ledgerAccountFlag", "00"); //是否做分账 分账交易使 用00 01不做不传默认为不做分账
//reqData.put("ledgerAccountEffectTime", "00"); //分账有效时间 单位为天;是 否做分账选择 00 时该字段必传
// TODO: 2026/3/11 后端回调地址
reqData.put("notifyUrl", "http://193.112.94.36:8081/mall/order/notify"); //回调地址
//reqData.put("ylTrmNo", ""); //银联终端号
//reqData.put("terminalId", ""); //TQ机具编号
//reqData.put("deviceNo ", ""); //设备号
//reqData.put("identityFlag", ""); //是否是实名支付
//reqData.put("buyerIdType", "IDCARD"); //证件类型
//reqData.put("buyerIdNo", "410523198701054018"); //证件号
//reqData.put("buyerName", "张三"); //买家姓名
//reqData.put("mobileNum", ""); //手机号
//reqData.put("extend", ""); //备用
reqBean.setReqData(reqData);
String req = JSONObject.toJSONString(reqBean);
//请求数据json字符串示例
//String req= "{\"orgId\":\"26680846\",\"reqData\":{\"ordNo\":\"35081904944040745\",\"payType\":\"WECHAT\",\"customerIp\":\"\",\"subject\":\"聚合支付测试\",\"amt\":\"0.01\",\"payWay\":\"02\",\"tradeSource\":\"02\",\"userId\":\"2088101117955611\",\"trmIp\":\"172.16.2.1\",\"mno\":\"399190910000387\"},\"reqId\":\"08794fa24dcb467992a06b126e542be4\",\"signType\":\"RSA\",\"timestamp\":\"2020-04-03 17:51:34\",\"version\":\"1.0\"}";
System.out.println("req:" + req);
//此处不要改变reqData里面值的顺序用LinkedHashMap
HashMap reqMap = JSON.parseObject(req, LinkedHashMap.class, Feature.OrderedField);
//组装加密串
String signContent = RSASignature.getOrderContent(reqMap);
System.out.println("拼接后的参数:" + signContent);
//sign
String sign = RSASignature.encryptBASE64(RSASignature.sign(signContent, privateKey));
System.out.println("============签名:" + sign);
reqMap.put("sign", sign);
String reqStr = JSON.toJSONString(reqMap);
System.out.println("请求参数:" + reqMap);
System.out.println("请求参数:" + reqStr);
String url = "https://openapi.tianquetech.com/order/reverseScan";
String resultJson = HttpUtils.connectPostUrl(url, reqStr);
log.info("返回信息" + resultJson);
System.out.println("返回信息:" + resultJson);
//不要对reqData排序 所以用LinkedHashMap
HashMap<String, Object> result = JSON.parseObject(resultJson, LinkedHashMap.class, Feature.OrderedField);
if ("0000".equals(result.get("code"))) {
//验签
String signResult = result.get("sign").toString();
result.remove("sign");
String resultStr = RSASignature.getOrderContent(result);
System.out.println(resultStr);
//sign
String resultSign = RSASignature.encryptBASE64(RSASignature.sign(signContent, privateKey));
System.out.println("resultSign:" + resultSign);
//组装加密串
if (RSASignature.doCheck(resultStr, signResult, sxfPublic)) {
log.info("===================验签成功==============");
String respDataStr = result.get("respData").toString();
HashMap<String, Object> respData = JSON.parseObject(respDataStr, LinkedHashMap.class);
// 获取 tranSts 和 ordNo
String tranSts = Optional.ofNullable(respData)
.map(m -> m.get("tranSts"))
.map(Object::toString)
.orElse("");
String ordNo = Optional.ofNullable(respData)
.map(m -> m.get("ordNo"))
.map(Object::toString)
.orElse("");
//被扫免密同步返回支付结果,不推送异步通知。 同步去修改支付状态,扣减库存
//被扫输密,推送异步通知。 通过回调接口修改支付状态,扣减库存
if ("SUCCESS".equals(tranSts)) {
// 4. 查询订单(加行锁)
Order order = orderMapper.selectForUpdateByOrderNo(ordNo);
if (order == null) {
log.error("订单不存在, orderNo:{}", ordNo);
throw new ServiceException("订单不存在");
}
// 5. 检查订单状态
if (order.getPayStatus() == 2) {
log.info("订单已支付, orderNo:{}", ordNo);
throw new ServiceException("订单已支付");
}
if (order.getPayStatus() != 0) {
log.error("订单状态错误, orderNo:{}, status:{}",
ordNo, order.getPayStatus());
throw new ServiceException("订单状态错误");
}
// 7. 查询订单明细
List<OrderItem> items = orderMapper.selectByOrderId(order.getId());
// 8. 扣减实际库存
for (OrderItem item : items) {
int result1 = productMapper.reduceStock(
order.getStoreId(),
item.getProductId(),
item.getQuantity()
);
if (result1 <= 0) {
log.error("库存扣减失败, storeId:{}, productId:{}, quantity:{}",
order.getStoreId(), item.getProductId(), item.getQuantity());
throw new ServiceException("库存扣减失败");
}
}
// 9. 更新订单状态为已支付
Order updateOrder = new Order();
updateOrder.setId(order.getId());
updateOrder.setPayStatus(2);
updateOrder.setPayTime(new Date());
int updated = orderMapper.updateById(updateOrder);
if (updated <= 0) {
log.error("订单状态更新失败, orderNo:{}",ordNo);
throw new ServiceException("订单状态更新失败");
}
log.info("支付同步处理成功, orderNo:{}, storeId:{}",
ordNo, order.getStoreId());
}
System.out.println("===================验签成功==============");
}
log.info("===================被扫支付==============" + resultJson);
return resultJson;
}
throw new ServiceException(resultJson);
}
@Override
public AjaxResult tradeQuery(TradeQueryDTO tradeQueryDTO) throws Exception {
//合作方私钥(替换成自己的)
String privateKey = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDM35qbWVNdBAU+oai8EVE3iSpA9wHAt4wOs+SufbkE/FNoMgp4EMrFNrdNkaKLQq4cRzqoNlJY+DZSL54dm7ekKZP/SLhWwNi9tJvhKsFpw6SRwthlKyA8lCW2mzFffFqLhGAayNoZTmHwkq8dotvecARVbiFtakzzFy/O7ATmzw3i57yaVgX2RtD8WWA7MGg5CQP1vvIDlXqd40SQcrJY6GNNv07Brv3fpyW2nXbvrxbOx+87NnG77/R7o8eFcWJF+yrqE1xeN3hDvHBFMFyf1M39Zig2I75vKRvHGqftAZuvlDG0nInwcSiMTE7wDsXvYzRnOsgJNVET7aFhzOvFAgMBAAECggEATjB4nRl2S2Whasnf0Ab77CoZSjn7HUXv2hymBkJNPq3eV0Hh5Pjjqmi7hIs0cdm9AWXd6RzySKpScQNjYQFxEOIdbayfTdzD24L2ivT6ZBca09Z9J/9Rik0wJ4ULTtny5JRc6VwGgOn9Twdsde8V4sr1nHUvpMaRH6WVxW8mX7+udgbK0Uxm5IFROeKGz0DSuG8SBpDhD2EB6P2Q+1fpna17yP1R08HtZNU6lQx7yMesaJuvKkwf4YiSjIhUtdbUXGhmbG5LFzoj3oRs25W68bIOQjT9i196hPQOYj6C7yz72nuY2rwFHhtsocfQRULWOHtsnWts9epvuFruDTdq8QKBgQD8bZhQkyQm6VTP0Hmv6sV9SXXKH0rb2V8hml7DxcpFl+l2/9UesH3qMoFKZ5C1YdPKZBNlD7fp5RxjtJ8MdwIp8MxRgMf/bZpIOx1Lfum1H+dKh5LF+oC4GGtJ5jqnwozNPog4keUn+bqKhiza9snA+6jy446A1Y8GGWlZwUGkVwKBgQDPxb7FZmOcotNnh8mS5pcx6d22UhBHIHz/X/dcjNQuI8tIRaIvkJm/4lfuxejCz3LY9k+VSQ96ZM2L5OqZGDlR7aMXQ3/FhuS4PEkNcDvccQPoig7U5zUUn2Jdqom6Lngh7Ka8B1v24ght0yXHHNChmMg/DJ42EzCnjpydWs6/QwKBgA9qGd0BvzlpEjbGgkfNzFWEQN8g3g9izL5ekN7fmyR4zFbp9He1S1sbzm1euaV53dcEGXMYbKCpzvv/sZ6vPcCV5cQsWwosBTnX8kgD7f2Tfyo78SiJzYZwZ0zR9E7+QF7gLK1Xq2ivhUakPuT4IQXZ2E1MvAz9/Yff0WEbvghtAoGAejFzC8c2yDUenaHHU+TXgNxor0Q+HIan3M0EvmJ4mxYkBMInK1AgjDBCxMOSK5gzlBPwI/0O5E2KcT7VFeqgM2XN5+2jpHi75PpXgFbEbdXtlYI0TNQZbKJ8CFg2nc+ciV8ThDvTwzOV/3kRm7N/o7ol8qaqGWVZ1QFTbFuugd8CgYBAsNVKCEHGZAwS5Csv/IBopcrJOJknmEHeAT+VwxtZK7yOJ4ZSNizzofu847YGO4uEhfcaC8lYxnj8XskuMG2KChbbxEA+qlnL/oMIwVrpE0ZbFmQ4WRTUfVQT+zCHMPuuuj5tLPTSMjQLGlq+r3XRzwUaX98J9GLKgHG7tXlphw==";
//随行付公钥
String sxfPublic = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjo1+KBcvwDSIo+nMYLeOJ19Ju4ii0xH66ZxFd869EWFWk/EJa3xIA2+4qGf/Ic7m7zi/NHuCnfUtUDmUdP0JfaZiYwn+1Ek7tYAOc1+1GxhzcexSJLyJlR2JLMfEM+rZooW4Ei7q3a8jdTWUNoak/bVPXnLEVLrbIguXABERQ0Ze0X9Fs0y/zkQFg8UjxUN88g2CRfMC6LldHm7UBo+d+WlpOYH7u0OTzoLLiP/04N1cfTgjjtqTBI7qkOGxYs6aBZHG1DJ6WdP+5w+ho91sBTVajsCxAaMoExWQM2ipf/1qGdsWmkZScPflBqg7m0olOD87ymAVP/3Tcbvi34bDfwIDAQAB";
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
String dataStr = df.format(new Date());
ApiRequestBean<JSONObject> reqBean = new ApiRequestBean<JSONObject>();
reqBean.setOrgId("57594783"); //机构id
reqBean.setReqId(UUID.randomUUID().toString().replaceAll("-", ""));
reqBean.setSignType("RSA");
reqBean.setTimestamp(dataStr);
reqBean.setVersion("1.0");
JSONObject reqData = new JSONObject();
//业务参数
reqData.put("ordNo", tradeQueryDTO.getOrdNo()); //商户订单号
reqData.put("mno", "399260311280126"); //商户编号
reqData.put("trmIp", "172.16.2.1");
reqBean.setReqData(reqData);
String req = JSONObject.toJSONString(reqBean);
//此处不要改变reqData里面值的顺序用LinkedHashMap
HashMap reqMap = JSON.parseObject(req, LinkedHashMap.class, Feature.OrderedField);
//组装加密串
String signContent = RSASignature.getOrderContent(reqMap);
//sign
String sign = RSASignature.encryptBASE64(RSASignature.sign(signContent, privateKey));
reqMap.put("sign", sign);
String reqStr = JSON.toJSONString(reqMap);
String url = "https://openapi.tianquetech.com/query/tradeQuery";
String resultJson = HttpUtils.connectPostUrl(url, reqStr);
//不要对reqData排序 所以用LinkedHashMap
HashMap<String, Object> result = JSON.parseObject(resultJson, LinkedHashMap.class, Feature.OrderedField);
if ("0000".equals(result.get("code"))) {
//验签
String signResult = result.get("sign").toString();
result.remove("sign");
String resultStr = RSASignature.getOrderContent(result);
System.out.println(resultStr);
//sign
String resultSign = RSASignature.encryptBASE64(RSASignature.sign(signContent, privateKey));
System.out.println("resultSign:" + resultSign);
//组装加密串
if (RSASignature.doCheck(resultStr, signResult, sxfPublic)) {
log.info("===================验签成功==============");
PaymentResponseVO paymentResponseVO = JSON.parseObject(resultJson, PaymentResponseVO.class);
return AjaxResult.success(paymentResponseVO);
}
}
return AjaxResult.error(resultJson);
}
@Override
public OrderCountVO getOrderCount(String storeId) {
return orderMapper.getOrderCount(storeId);
}
//调用支付
public String pay(String orderNo, String totalAmount) throws Exception {
//合作方私钥(替换成自己的)
String privateKey = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDM35qbWVNdBAU+oai8EVE3iSpA9wHAt4wOs+SufbkE/FNoMgp4EMrFNrdNkaKLQq4cRzqoNlJY+DZSL54dm7ekKZP/SLhWwNi9tJvhKsFpw6SRwthlKyA8lCW2mzFffFqLhGAayNoZTmHwkq8dotvecARVbiFtakzzFy/O7ATmzw3i57yaVgX2RtD8WWA7MGg5CQP1vvIDlXqd40SQcrJY6GNNv07Brv3fpyW2nXbvrxbOx+87NnG77/R7o8eFcWJF+yrqE1xeN3hDvHBFMFyf1M39Zig2I75vKRvHGqftAZuvlDG0nInwcSiMTE7wDsXvYzRnOsgJNVET7aFhzOvFAgMBAAECggEATjB4nRl2S2Whasnf0Ab77CoZSjn7HUXv2hymBkJNPq3eV0Hh5Pjjqmi7hIs0cdm9AWXd6RzySKpScQNjYQFxEOIdbayfTdzD24L2ivT6ZBca09Z9J/9Rik0wJ4ULTtny5JRc6VwGgOn9Twdsde8V4sr1nHUvpMaRH6WVxW8mX7+udgbK0Uxm5IFROeKGz0DSuG8SBpDhD2EB6P2Q+1fpna17yP1R08HtZNU6lQx7yMesaJuvKkwf4YiSjIhUtdbUXGhmbG5LFzoj3oRs25W68bIOQjT9i196hPQOYj6C7yz72nuY2rwFHhtsocfQRULWOHtsnWts9epvuFruDTdq8QKBgQD8bZhQkyQm6VTP0Hmv6sV9SXXKH0rb2V8hml7DxcpFl+l2/9UesH3qMoFKZ5C1YdPKZBNlD7fp5RxjtJ8MdwIp8MxRgMf/bZpIOx1Lfum1H+dKh5LF+oC4GGtJ5jqnwozNPog4keUn+bqKhiza9snA+6jy446A1Y8GGWlZwUGkVwKBgQDPxb7FZmOcotNnh8mS5pcx6d22UhBHIHz/X/dcjNQuI8tIRaIvkJm/4lfuxejCz3LY9k+VSQ96ZM2L5OqZGDlR7aMXQ3/FhuS4PEkNcDvccQPoig7U5zUUn2Jdqom6Lngh7Ka8B1v24ght0yXHHNChmMg/DJ42EzCnjpydWs6/QwKBgA9qGd0BvzlpEjbGgkfNzFWEQN8g3g9izL5ekN7fmyR4zFbp9He1S1sbzm1euaV53dcEGXMYbKCpzvv/sZ6vPcCV5cQsWwosBTnX8kgD7f2Tfyo78SiJzYZwZ0zR9E7+QF7gLK1Xq2ivhUakPuT4IQXZ2E1MvAz9/Yff0WEbvghtAoGAejFzC8c2yDUenaHHU+TXgNxor0Q+HIan3M0EvmJ4mxYkBMInK1AgjDBCxMOSK5gzlBPwI/0O5E2KcT7VFeqgM2XN5+2jpHi75PpXgFbEbdXtlYI0TNQZbKJ8CFg2nc+ciV8ThDvTwzOV/3kRm7N/o7ol8qaqGWVZ1QFTbFuugd8CgYBAsNVKCEHGZAwS5Csv/IBopcrJOJknmEHeAT+VwxtZK7yOJ4ZSNizzofu847YGO4uEhfcaC8lYxnj8XskuMG2KChbbxEA+qlnL/oMIwVrpE0ZbFmQ4WRTUfVQT+zCHMPuuuj5tLPTSMjQLGlq+r3XRzwUaX98J9GLKgHG7tXlphw==";
//随行付公钥
String sxfPublic = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjo1+KBcvwDSIo+nMYLeOJ19Ju4ii0xH66ZxFd869EWFWk/EJa3xIA2+4qGf/Ic7m7zi/NHuCnfUtUDmUdP0JfaZiYwn+1Ek7tYAOc1+1GxhzcexSJLyJlR2JLMfEM+rZooW4Ei7q3a8jdTWUNoak/bVPXnLEVLrbIguXABERQ0Ze0X9Fs0y/zkQFg8UjxUN88g2CRfMC6LldHm7UBo+d+WlpOYH7u0OTzoLLiP/04N1cfTgjjtqTBI7qkOGxYs6aBZHG1DJ6WdP+5w+ho91sBTVajsCxAaMoExWQM2ipf/1qGdsWmkZScPflBqg7m0olOD87ymAVP/3Tcbvi34bDfwIDAQAB";
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
String dataStr = df.format(new Date());
ApiRequestBean<JSONObject> reqBean = new ApiRequestBean<JSONObject>();
reqBean.setOrgId("57594783"); //机构id
reqBean.setReqId(UUID.randomUUID().toString().replaceAll("-", ""));
reqBean.setSignType("RSA");
reqBean.setTimestamp(dataStr);
reqBean.setVersion("1.0");
JSONObject reqData = new JSONObject();
//业务参数
reqData.put("ordNo", orderNo); //商户订单号
reqData.put("mno", "399260311280126"); //商户编号
//reqData.put("subMechId", ""); //子商户号
reqData.put("amt", totalAmount); //订单总金额
//reqData.put("discountAmt", ""); //参与优惠金额
//reqData.put("unDiscountAmt", ""); //不参与优惠金额
// TODO: 2026/3/11 后续前端传
reqData.put("payType", "WECHAT"); //支付渠道
// TODO: 2026/3/11 后续前端传
reqData.put("payWay", "02"); //支付方式 02 公众号/服 务窗/js支付 03 小程序
//reqData.put("timeExpire", "10"); //订单失效时间, 以分钟为单位
//reqData.put("limitPay", "00"); //限制卡类型: 00-全部 01-限定不能使 用信用卡支付 默认值 00
reqData.put("subject", "聚合支付测试"); //订单标题
//reqData.put("hbFqNum", "6"); //花呗分期数,仅可上送 6 或 12
//reqData.put("hbFqPercent", "0"); //卖家承担分期 服务费比例,仅支持上送 0 或 100
// TODO: 2026/3/11 后续前端传
reqData.put("tradeSource", "02"); //交易来源 01服务商02收银台03硬件
reqData.put("trmIp", "172.16.2.1");
reqData.put("customerIp", ""); //持卡人ip地址银联js支付时必传
// TODO: 2026/3/11 后续前端传code,后端去获取对应的id
reqData.put("userId", "o3LXk25WUnC4U2lGeuEvLLu0fjBs"); //用户号 微信openid 支付宝userid银联userid微信&支付宝必传银联js为非必传
//reqData.put("subAppid", "wx24210004370ec43b"); //微信子公众号
//reqData.put("outFrontUrl", ""); //js 支付,前台 成功通知地址
//reqData.put("outFrontFailUrl", ""); //js 支付,前台 事变通知地址
// TODO: 2026/3/11 后端回调地址
reqData.put("notifyUrl", "http://193.112.94.36:8081/mall/order/notify"); //回调地址
//reqData.put("needReceipt", "00"); //电子发票功能 微信开具电子 发票使用
//reqData.put("ledgerAccountFlag", "00"); //是否做分账 分账交易使 用00 01不做不传默认为不做分账
//reqData.put("ledgerAccountEffectTime", "00"); //分账有效时间 单位为天;是 否做分账选择 00 时该字段必传
//reqData.put("ylTrmNo", ""); //银联终端号
//reqData.put("terminalId", ""); //TQ机具编号
//reqData.put("identityFlag", ""); //是否是实名支付
//reqData.put("buyerIdType", "IDCARD"); //证件类型
//reqData.put("buyerIdNo", "410523198701054018"); //证件号
//reqData.put("buyerName", "张三"); //买家姓名
//reqData.put("mobileNum", ""); //手机号
//reqData.put("extend", ""); //备用
//reqData.put("goodsTag", "00"); //订单优惠标识 0001
//reqData.put("couponDetail", ""); //优惠详情信息,见下面三个字段
//reqData.put("costPrice", "200"); //订单原价保留两 位小数;微信 独有
//reqData.put("receiptId ", "123456789"); //商品小票
//reqData.put("goodsDetail", "123456789"); //单品优惠信息使用 json 数组格式提交
//reqData.put("goodsId", "200"); //商品编码
//reqData.put("thirdGoodsId", "12345678"); //微信/支付宝侧商品码
//reqData.put("goodsName", "苹果电脑"); //商品名称
//reqData.put("quantity", "1"); //商品数量
//reqData.put("price", "1.01"); //商品单价
//reqData.put("goodsCategory", ""); //商品类目;支 付宝独有
//reqData.put("categoriesTree", "124868003|126232002|126252004"); //商品类目树
//reqData.put("goodsDesc", ""); //商品描述;支 付宝独有
//reqData.put("showUrl", ""); //商品展示地址 url支付宝独有
reqBean.setReqData(reqData);
String req = JSONObject.toJSONString(reqBean);
//请求数据json字符串示例
//String req= "{\"orgId\":\"26680846\",\"reqData\":{\"ordNo\":\"35081904944040745\",\"payType\":\"WECHAT\",\"customerIp\":\"\",\"subject\":\"聚合支付测试\",\"amt\":\"0.01\",\"payWay\":\"02\",\"tradeSource\":\"02\",\"userId\":\"2088101117955611\",\"trmIp\":\"172.16.2.1\",\"mno\":\"399190910000387\"},\"reqId\":\"08794fa24dcb467992a06b126e542be4\",\"signType\":\"RSA\",\"timestamp\":\"2020-04-03 17:51:34\",\"version\":\"1.0\"}";
System.out.println("req:" + req);
//此处不要改变reqData里面值的顺序用LinkedHashMap
HashMap reqMap = JSON.parseObject(req, LinkedHashMap.class, Feature.OrderedField);
//组装加密串
String signContent = RSASignature.getOrderContent(reqMap);
System.out.println("拼接后的参数:" + signContent);
//sign
String sign = RSASignature.encryptBASE64(RSASignature.sign(signContent, privateKey));
System.out.println("============签名:" + sign);
reqMap.put("sign", sign);
String reqStr = JSON.toJSONString(reqMap);
System.out.println("请求参数:" + reqMap);
System.out.println("请求参数:" + reqStr);
String url = "https://openapi.tianquetech.com/order/jsapiScan";
String resultJson = HttpUtils.connectPostUrl(url, reqStr);
log.info("返回信息" + resultJson);
System.out.println("返回信息:" + resultJson);
//不要对reqData排序 所以用LinkedHashMap
HashMap<String, Object> result = JSON.parseObject(resultJson, LinkedHashMap.class, Feature.OrderedField);
if ("0000".equals(result.get("code"))) {
//验签
String signResult = result.get("sign").toString();
result.remove("sign");
String resultStr = RSASignature.getOrderContent(result);
System.out.println(resultStr);
//sign
String resultSign = RSASignature.encryptBASE64(RSASignature.sign(signContent, privateKey));
System.out.println("resultSign:" + resultSign);
//组装加密串
if (RSASignature.doCheck(resultStr, signResult, sxfPublic)) {
log.info("===================验签成功==============");
System.out.println("===================验签成功==============");
}
return resultJson;
}
throw new ServiceException(resultJson);
}
/**
* keyID
*/
private String generateLockKey(Integer storeId, List<OrderItem> items) {
// 对商品ID排序
String productIds = items.stream()
.map(item -> String.valueOf(item.getProductId()))
.sorted()
.collect(Collectors.joining("_"));
// 组合门店ID和商品ID
String originalKey = storeId + ":" + productIds;
// 计算MD5避免key过长
String hash = DigestUtils.md5DigestAsHex(originalKey.getBytes());
return "lock:store:products:" + hash;
}
/**
*
*/
private String generateOrderNo() {
// 格式:年月日时分秒 + 雪花算法ID后8位
String dateStr = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
long snowflakeId = IdUtil.getSnowflake().nextId();
String suffix = String.valueOf(snowflakeId).substring(8);
return dateStr + suffix;
}
}

View File

@ -4,9 +4,11 @@ package com.ruoyi.web.service.impl;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.web.domain.Product;
import com.ruoyi.web.dto.BatchBarcodeQueryDTO;
import com.ruoyi.web.dto.BatchStoreDTO;
import com.ruoyi.web.mapper.ProductMapper;
import com.ruoyi.web.service.ProductService;
import com.ruoyi.web.vo.ProductVo;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@ -79,4 +81,9 @@ public class ProductServiceImpl implements ProductService {
}
return productMapper.batchProductById(dto.getIds());
}
@Override
public List<Product> getByBarcodes(BatchBarcodeQueryDTO dto) {
return productMapper.getByBarcodes(dto);
}
}

View File

@ -0,0 +1,144 @@
package com.ruoyi.web.utils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
public class HttpUtils {
/**
* SSL
* @return
* @throws NoSuchAlgorithmException
* @throws KeyStoreException
* @throws KeyManagementException
*/
public static SSLConnectionSocketFactory getSSLConnectionSocketFactory() throws NoSuchAlgorithmException,
KeyStoreException, KeyManagementException {
SSLContextBuilder context = new SSLContextBuilder().useProtocol("TLSv1.2");
context.loadTrustMaterial(null, new TrustStrategy() {
@Override
public boolean isTrusted(X509Certificate[] arg0, String arg1)
throws CertificateException {
return true;
}
});
return new SSLConnectionSocketFactory(context.build());
}
/**
* GET
* @param url
* @return
* @throws KeyManagementException
* @throws NoSuchAlgorithmException
* @throws KeyStoreException
* @throws ClientProtocolException
* @throws IOException
*/
public static String connectGetUrl(String url) throws KeyManagementException,
NoSuchAlgorithmException, KeyStoreException, ClientProtocolException, IOException {
SSLConnectionSocketFactory sslsf = getSSLConnectionSocketFactory();
CloseableHttpClient client = HttpClients.custom().setSSLSocketFactory(sslsf).build();
RequestConfig config = RequestConfig.custom().setSocketTimeout(10000).setConnectTimeout(5000).build();
HttpGet httpGet = null;
CloseableHttpResponse resp = null;
String jsonString = "";
try {
httpGet = new HttpGet(url);
httpGet.setConfig(config);
resp = client.execute(httpGet);
HttpEntity entity = resp.getEntity();
jsonString = EntityUtils.toString(entity, "UTF-8");
} finally {
if (resp != null) {
try {
resp.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (httpGet != null) {
httpGet.releaseConnection();
}
}
return jsonString;
}
/**
* POST
* @param url
* @param jsonParm
* @return
* @throws KeyManagementException
* @throws NoSuchAlgorithmException
* @throws KeyStoreException
* @throws ClientProtocolException
* @throws IOException
*/
public static String connectPostUrl(String url, String jsonParm) throws KeyManagementException,
NoSuchAlgorithmException, KeyStoreException, ClientProtocolException, IOException {
SSLConnectionSocketFactory sslsf = getSSLConnectionSocketFactory();
CloseableHttpClient client = HttpClients.custom().setSSLSocketFactory(sslsf).build();
RequestConfig config = RequestConfig.custom().setSocketTimeout(40000).setConnectTimeout(10000).build();
HttpPost httpPost = null;
CloseableHttpResponse resp = null;
try {
httpPost = new HttpPost(url);
httpPost.setConfig(config);
StringEntity params = new StringEntity(jsonParm,"UTF-8");
params.setContentType("application/json");
httpPost.setEntity(params);
resp = client.execute(httpPost);
HttpEntity entity = resp.getEntity();
String jsonString = EntityUtils.toString(entity, "UTF-8");
if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
return jsonString;
}
}catch (IOException e) {
if (e instanceof org.apache.http.conn.ConnectTimeoutException) {
throw new org.apache.http.conn.ConnectTimeoutException("connect timed out");
}
if (e instanceof java.net.SocketTimeoutException) {
throw new java.net.SocketTimeoutException("Read timed out");
}
throw new IOException("IOException");
} finally {
if (resp != null) {
try {
resp.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (httpPost != null) {
httpPost.releaseConnection();
}
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}

View File

@ -0,0 +1,105 @@
package com.ruoyi.web.utils;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.*;
/**
* RSA
*/
public class RSASignature {
/**
*
*/
public static final String SIGN_ALGORITHMS = "SHA1WithRSA";
/**
*
* @param sortedParams
* @return
*/
public static String getOrderContent(Map<String, Object> requestParam) {
Map<String, Object> sortedParams = new TreeMap<String, Object>();
if ((requestParam != null) && (requestParam.size() > 0)) {
sortedParams.putAll(requestParam);
}
StringBuffer content = new StringBuffer();
List<String> keys = new ArrayList<String>(sortedParams.keySet());
Collections.sort(keys);
int index = 0;
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
Object value = sortedParams.get(key);
if (key!=null &&!"".equals(key)&& value != null) {
content.append((index == 0 ? "" : "&") + key + "=" + value);
index++;
}
}
return content.toString();
}
/**
* rsa
* @param content
* @param privateKey
* @return
*/
public static byte[] sign(String content, String privateKey) {
try {
PKCS8EncodedKeySpec priPKCS8 = new PKCS8EncodedKeySpec(decryptBASE64(privateKey));
KeyFactory keyf = KeyFactory.getInstance("RSA");
PrivateKey priKey = keyf.generatePrivate(priPKCS8);
java.security.Signature signature = java.security.Signature.getInstance(SIGN_ALGORITHMS);
signature.initSign(priKey);
signature.update(content.getBytes("UTF-8"));
byte[] signed = signature.sign();
return signed;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* RSA
* @param content
* @param sign
* @param publicKey
* @return
*/
public static boolean doCheck(String content, String sign, String publicKey) {
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
byte[] encodedKey = decryptBASE64(publicKey);
PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));
java.security.Signature signature = java.security.Signature.getInstance(SIGN_ALGORITHMS);
signature.initVerify(pubKey);
signature.update(content.getBytes("UTF-8"));
boolean bverify = signature.verify(decryptBASE64(sign));
return bverify;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public static String encryptBASE64(byte[] key) throws Exception {
String base64encodedString = Base64.getEncoder().encodeToString(key);
return base64encodedString.replaceAll("[\\s*\t\n\r]", "");
}
public static byte[] decryptBASE64(String key) throws Exception {
byte[] base64decodedBytes = Base64.getDecoder().decode(key.replaceAll("[\\s*\t\n\r]", ""));
return base64decodedBytes;
}
}

View File

@ -0,0 +1,13 @@
package com.ruoyi.web.vo;
import lombok.Data;
@Data
public class OrderCountVO {
//支付订单数量
private String paidOrderCount;
//支付订单金额
private String totalPayAmount;
}

View File

@ -0,0 +1,11 @@
package com.ruoyi.web.vo;
import lombok.Data;
@Data
public class OrderVo {
private String orderNo;
private String amount;
}

View File

@ -0,0 +1,234 @@
package com.ruoyi.web.vo;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* VO
*/
@Data
public class PaymentResponseVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
*
*/
private String msg;
/**
*
*/
private String code;
/**
*
*/
private String sign;
/**
*
*/
private String signType;
/**
* ID
*/
private String orgId;
/**
* ID
*/
private String reqId;
/**
*
*/
private RespData respData;
/**
*
*/
@Data
public static class RespData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* yyyyMMddHHmmss
*/
private String payTime;
/**
*
*/
private String bizCode;
/**
*
*/
private String subject;
/**
*
*/
private String settleAmt;
/**
* 01- 02-
*/
private String payWay;
/**
*
*/
private String sxfUuid;
/**
*
*/
private String payBank;
/**
*
*/
private String uuid;
/**
*
*/
private String scene;
/**
* yyyyMMddHHmmss
*/
private String tranTime;
/**
*
*/
private String settlementBatchNo;
/**
* WECHAT/ALIPAY
*/
private String payType;
/**
*
*/
private String ordNo;
/**
*
*/
private String szltFlag;
/**
*
*/
private String bizMsg;
/**
*
*/
private String recFeeAmt;
/**
*
*/
private String recFeeRate;
/**
*
*/
private String szltRecfeeAmt;
/**
*
*/
private String oriTranAmt;
/**
* ID
*/
private String channelId;
/**
* yyyyMMddHHmmss
*/
private String finishTime;
/**
* SUCCESS-
*/
private String tranSts;
/**
*
*/
private String pointAmount;
/**
*
*/
private String ledgerAccountFlag;
/**
*
*/
private String tradeCode;
/**
* yyyyMMdd
*/
private String clearDt;
/**
*
*/
private String transactionId;
/**
*
*/
private String subMechId;
/**
*
*/
private String totalOffstAmt;
/**
*
*/
private String tradeMsg;
/**
*
*/
private String drType;
/**
* BigDecimal
*/
public BigDecimal getSettleAmtDecimal() {
return settleAmt != null ? new BigDecimal(settleAmt) : BigDecimal.ZERO;
}
/**
* BigDecimal
*/
public BigDecimal getOriTranAmtDecimal() {
return oriTranAmt != null ? new BigDecimal(oriTranAmt) : BigDecimal.ZERO;
}
/**
*
*/
public boolean isSuccess() {
return "SUCCESS".equals(tranSts) && "0000".equals(bizCode);
}
}
}

View File

@ -0,0 +1,12 @@
package com.ruoyi.web.vo;
import lombok.Data;
@Data
public class ProductVo {
private Long id;
private String productName;
private String storePrice;
private String mainImage;
private String productBarCode; // 商品条码
}

View File

@ -0,0 +1,80 @@
<?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.ruoyi.web.mapper.DeviceMapper">
<resultMap id="BaseResultMap" type="com.ruoyi.web.domain.Device">
<id property="id" column="id"/>
<result column="store_id" property="storeId"/>
<result column="device_code" property="deviceCode"/>
<result column="device_name" property="deviceName"/>
<result column="device_type" property="deviceType"/>
<result column="device_brand" property="deviceBrand"/>
<result column="device_model" property="deviceModel"/>
<result column="channel" property="channel"/>
<result column="mac_address" property="macAddress"/>
<result column="auth_code" property="authCode"/>
<result column="install_date" property="installDate"/>
<result column="expire_date" property="expireDate"/>
<result column="device_status" property="deviceStatus"/>
<result column="last_heartbeat" property="lastHeartbeat"/>
<result column="remark" property="remark"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
<result column="create_by" property="createBy"/>
<result column="update_by" property="updateBy"/>
</resultMap>
<select id="getDevice" resultMap="BaseResultMap">
select *
from mall_device_info
where store_id = #{storeId}
and del_flag = 0
</select>
<select id="getSheXiangTou" resultMap="BaseResultMap">
select *
from mall_device_info
where store_id = #{storeId}
and del_flag = 0 and device_type = 1
</select>
<insert id="addDevice" parameterType="Device">
insert into mall_device_info (
<if test="storeId != null">store_id,</if>
<if test="deviceCode != null and deviceCode != ''">device_code,</if>
<if test="deviceName != null and deviceName != ''">device_name,</if>
<if test="deviceType != null">device_type,</if>
<if test="deviceBrand != null and deviceBrand != ''">device_brand,</if>
<if test="deviceModel != null and deviceModel != ''">device_model,</if>
<if test="channel != null and channel != ''">channel,</if>
<if test="macAddress != null and macAddress != ''">mac_address,</if>
<if test="authCode != null and authCode != ''">auth_code,</if>
<if test="installDate != null ">install_date,</if>
<if test="expireDate != null">expire_date,</if>
<if test="deviceStatus != null ">device_status,</if>
<if test="lastHeartbeat != null ">last_heartbeat,</if>
<if test="remark != null and remark != ''">remark,</if>
<if test="createBy != null and createBy != ''">create_by,</if>
create_time
)values(
<if test="storeId != null">#{storeId},</if>
<if test="deviceCode != null and deviceCode != ''">#{deviceCode},</if>
<if test="deviceName != null and deviceName != ''">#{deviceName},</if>
<if test="deviceType != null">#{deviceType},</if>
<if test="deviceBrand != null and deviceBrand != ''">#{deviceBrand},</if>
<if test="deviceModel != null and deviceModel != ''">#{deviceModel},</if>
<if test="channel != null and channel != ''">#{channel},</if>
<if test="macAddress != null and macAddress != ''">#{macAddress},</if>
<if test="authCode != null and authCode != ''">#{authCode},</if>
<if test="installDate != null">#{installDate},</if>
<if test="expireDate != null">#{expireDate},</if>
<if test="deviceStatus != null">#{deviceStatus},</if>
<if test="lastHeartbeat != null">#{lastHeartbeat},</if>
<if test="remark != null and remark != ''">#{remark},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
sysdate()
)
</insert>
</mapper>

View File

@ -0,0 +1,63 @@
<?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.ruoyi.web.mapper.DoorScanRecordMapper">
<resultMap id="BaseResultMap" type="com.ruoyi.web.domain.DoorScanRecord">
<id property="id" column="id"/>
<result column="operator_name" property="operatorName"/>
<result column="operator_phone" property="operatorPhone"/>
<result column="store_id" property="storeId"/>
<result column="shop_name" property="shopName"/>
<result column="scan_type" property="scanType"/>
<result column="scan_time" property="scanTime"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
<result column="create_by" property="createBy"/>
<result column="update_by" property="updateBy"/>
</resultMap>
<insert id="addDoorScanRecord">
insert into mall_scan_record (
<if test="storeId != null">store_id,</if>
<if test="operatorName != null and operatorName != ''">operator_name ,</if>
<if test="operatorPhone != null and operatorPhone != ''">operator_phone ,</if>
<if test="scanType != null and scanType != ''">scan_type ,</if>
<if test="shopName != null and shopName != ''">shop_name ,</if>
<if test="scanTime != null">scan_time ,</if>
<if test="createBy != null and createBy != ''">create_by,</if>
create_time
)values(
<if test="storeId != null">#{storeId},</if>
<if test="operatorName != null and operatorName != ''">#{operatorName},</if>
<if test="operatorPhone != null and operatorPhone != ''">#{operatorPhone},</if>
<if test="scanType != null and scanType != ''">#{scanType},</if>
<if test="shopName != null and shopName != ''">#{shopName},</if>
<if test="scanTime != null">#{scanTime},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
sysdate()
)
</insert>
<select id="selectScanRecordList" parameterType="DoorScanRecord" resultMap="BaseResultMap">
select * from mall_scan_record
<where>
del_flag = '0' and store_id = #{storeId}
<if test="operatorName != null and operatorName != ''">
AND operator_name = #{operatorName}
</if>
<if test="createTime != null">
AND DATE(create_time) = DATE(#{createTime})
</if>
</where>
</select>
<select id="getEnterCountByDate" resultType="java.lang.Integer">
SELECT COUNT(*)
FROM mall_scan_record
where scan_type = 0 and store_id = #{storeId}
AND DATE (create_time) = DATE (#{date})
</select>
</mapper>

View File

@ -0,0 +1,181 @@
<?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.ruoyi.web.mapper.InstallTaskMapper">
<resultMap id="BaseResultMap" type="com.ruoyi.web.domain.InstallTask">
<id property="id" column="id" />
<result property="taskNo" column="task_no" />
<result property="storeId" column="store_id" />
<result property="storeName" column="store_name" />
<result property="contactPerson" column="contact_person" />
<result property="contactPhone" column="contact_phone" />
<result property="deviceType" column="device_type" />
<result property="deviceModel" column="device_model" />
<result property="installAddress" column="install_address" />
<result property="expectedTime" column="expected_time" />
<result property="remark" column="remark" />
<result property="taskStatus" column="task_status" />
<result property="assigneeId" column="assignee_id" />
<result property="assigneeName" column="assignee_name" />
<result property="assignTime" column="assign_time" />
<result property="deviceId" column="device_id" />
<result property="deviceCode" column="device_code" />
<result property="authCode" column="auth_code" />
<result property="installTime" column="install_time" />
<result property="installPhoto" column="install_photo" />
<result property="completeRemark" column="complete_remark" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
</resultMap>
<!-- 新增安装任务 -->
<insert id="insertInstallTask" parameterType="com.ruoyi.web.domain.InstallTask" useGeneratedKeys="true" keyProperty="id">
INSERT INTO mall_install_task (
task_no, store_id, store_name, contact_person, contact_phone,
device_type, device_model, install_address, expected_time, remark,
task_status, del_flag, create_by, create_time
) VALUES (
#{taskNo}, #{storeId}, #{storeName}, #{contactPerson}, #{contactPhone},
#{deviceType}, #{deviceModel}, #{installAddress}, #{expectedTime}, #{remark},
0, '0', #{createBy}, #{createTime}
)
</insert>
<!-- 根据ID查询 -->
<select id="selectInstallTaskById" parameterType="long" resultMap="BaseResultMap">
SELECT * FROM mall_install_task
WHERE id = #{id} AND del_flag = '0'
</select>
<!-- 根据任务编号查询 -->
<select id="selectInstallTaskByTaskNo" parameterType="string" resultMap="BaseResultMap">
SELECT * FROM mall_install_task
WHERE task_no = #{taskNo} AND del_flag = '0'
</select>
<!-- 查询安装任务列表 -->
<select id="selectInstallTaskList" parameterType="com.ruoyi.web.domain.InstallTask" resultMap="BaseResultMap">
SELECT * FROM mall_install_task
WHERE del_flag = '0'
<if test="taskNo != null and taskNo != ''">
AND task_no LIKE CONCAT('%', #{taskNo}, '%')
</if>
<if test="storeId != null">
AND store_id = #{storeId}
</if>
<if test="storeName != null and storeName != ''">
AND store_name LIKE CONCAT('%', #{storeName}, '%')
</if>
<if test="contactPerson != null and contactPerson != ''">
AND contact_person LIKE CONCAT('%', #{contactPerson}, '%')
</if>
<if test="contactPhone != null and contactPhone != ''">
AND contact_phone LIKE CONCAT('%', #{contactPhone}, '%')
</if>
<if test="deviceType != null">
AND device_type = #{deviceType}
</if>
<if test="taskStatus != null">
AND task_status = #{taskStatus}
</if>
<if test="assigneeId != null">
AND assignee_id = #{assigneeId}
</if>
<if test="assigneeName != null and assigneeName != ''">
AND assignee_name LIKE CONCAT('%', #{assigneeName}, '%')
</if>
<if test="params.beginTime != null and params.beginTime != ''">
AND create_time >= #{params.beginTime}
</if>
<if test="params.endTime != null and params.endTime != ''">
AND create_time &lt;= #{params.endTime}
</if>
ORDER BY create_time DESC
</select>
<!-- 更新安装任务 -->
<update id="updateInstallTask" parameterType="com.ruoyi.web.domain.InstallTask">
UPDATE mall_install_task
<set>
<if test="contactPerson != null">contact_person = #{contactPerson},</if>
<if test="contactPhone != null">contact_phone = #{contactPhone},</if>
<if test="deviceType != null">device_type = #{deviceType},</if>
<if test="deviceModel != null">device_model = #{deviceModel},</if>
<if test="installAddress != null">install_address = #{installAddress},</if>
<if test="expectedTime != null">expected_time = #{expectedTime},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="taskStatus != null">task_status = #{taskStatus},</if>
<if test="assigneeId != null">assignee_id = #{assigneeId},</if>
<if test="assigneeName != null">assignee_name = #{assigneeName},</if>
<if test="assignTime != null">assign_time = #{assignTime},</if>
<if test="deviceId != null">device_id = #{deviceId},</if>
<if test="deviceCode != null">device_code = #{deviceCode},</if>
<if test="authCode != null">auth_code = #{authCode},</if>
<if test="installTime != null">install_time = #{installTime},</if>
<if test="installPhoto != null">install_photo = #{installPhoto},</if>
<if test="completeRemark != null">complete_remark = #{completeRemark},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
update_time = NOW()
</set>
WHERE id = #{id} AND del_flag = '0'
</update>
<!-- 指派任务 -->
<update id="assignTask" parameterType="com.ruoyi.web.domain.InstallTask">
UPDATE mall_install_task
<set>
<if test="taskStatus != null">task_status = #{taskStatus},</if>
<if test="assigneeId != null">assignee_id = #{assigneeId},</if>
<if test="assigneeName != null">assignee_name = #{assigneeName},</if>
<if test="assignTime != null">assign_time = #{assignTime},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
update_time = NOW()
</set>
WHERE id = #{id} AND del_flag = '0'
</update>
<!-- 完成任务安装 -->
<update id="completeTask" parameterType="com.ruoyi.web.domain.InstallTask">
UPDATE mall_install_task
<set>
<if test="taskStatus != null">task_status = #{taskStatus},</if>
<if test="deviceId != null">device_id = #{deviceId},</if>
<if test="deviceCode != null">device_code = #{deviceCode},</if>
<if test="authCode != null">auth_code = #{authCode},</if>
<if test="installTime != null">install_time = #{installTime},</if>
<if test="installPhoto != null">install_photo = #{installPhoto},</if>
<if test="completeRemark != null">complete_remark = #{completeRemark},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
update_time = NOW()
</set>
WHERE id = #{id} AND del_flag = '0'
</update>
<!-- 查询当天最大任务编号 -->
<select id="selectMaxTaskNoByDate" parameterType="string" resultType="string">
SELECT MAX(task_no) FROM mall_install_task
WHERE task_no LIKE CONCAT(#{prefix}, '%')
</select>
<!-- 逻辑删除单个 -->
<update id="deleteInstallTaskById" parameterType="long">
UPDATE mall_install_task
SET del_flag = '2', update_time = NOW()
WHERE id = #{id}
</update>
<!-- 批量逻辑删除 -->
<update id="deleteInstallTaskByIds" parameterType="string">
UPDATE mall_install_task
SET del_flag = '2', update_time = NOW()
WHERE id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">
#{id}
</foreach>
</update>
</mapper>

View File

@ -40,41 +40,88 @@
<collection property="orderItems" ofType="OrderItem" resultMap="OrderItemResult"/>
</resultMap>
<!-- 插入订单 -->
<insert id="insert" parameterType="com.ruoyi.web.domain.Order" useGeneratedKeys="true" keyProperty="id">
INSERT INTO mall_order (order_no, store_id, pay_amount, pay_status,
create_time, create_by)
VALUES (#{orderNo}, #{storeId}, #{payAmount}, #{payStatus},
NOW(), #{createBy})
</insert>
<!-- 批量插入订单明细 -->
<insert id="batchInsert" parameterType="list">
INSERT INTO mall_order_item (
order_id,
product_id,
product_name,
product_no,
main_image,
spec,
unit_price,
quantity,
subtotal,
create_time,
create_by
) VALUES
<foreach collection="list" item="item" separator=",">
(
#{item.orderId},
#{item.productId},
#{item.productName},
#{item.productNo},
#{item.mainImage},
#{item.spec},
#{item.unitPrice},
#{item.quantity},
#{item.subtotal},
NOW(),
#{item.createBy}
)
</foreach>
</insert>
<!-- 根据ID更新订单选择性更新 -->
<update id="updateById" parameterType="com.ruoyi.web.domain.Order">
UPDATE mall_order
<set>
<if test="orderNo != null">order_no = #{orderNo},</if>
<if test="payTime != null">pay_time = #{payTime},</if>
<if test="payStatus != null">pay_status = #{payStatus},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
update_time = NOW()
</set>
WHERE
id = #{id}
AND del_flag = '0'
</update>
<!-- 查询订单列表(包含商品明细) -->
<select id="selectOrderList" parameterType="Order" resultMap="OrderWithItemsResult">
<select id="selectOrderList" resultMap="OrderWithItemsAndProductResultMap">
SELECT
o.id,
o.order_no,
o.store_id,
o.pay_amount,
o.pay_time,
o.pay_status,
o.create_time,
o.update_time,
o.del_flag,
o.pay_time,
o.create_by,
o.update_by,
o.create_time,
oi.id as item_id,
oi.order_id,
oi.product_id,
oi.product_name,
oi.product_no,
oi.main_image,
oi.spec,
oi.unit_price,
oi.quantity,
oi.subtotal,
oi.del_flag as item_del_flag,
oi.create_by as item_create_by,
oi.create_time as item_create_time
oi.order_id as item_order_id,
oi.product_id as item_product_id,
oi.quantity as item_quantity,
oi.subtotal as item_subtotal,
p.product_bar_code as product_bar_code,
p.product_name as item_product_name,
p.main_image as item_main_image
FROM mall_order o
LEFT JOIN mall_order_item oi ON o.id = oi.order_id AND oi.del_flag = '0'
LEFT JOIN mall_order_item oi ON o.id = oi.order_id
LEFT JOIN mall_product_store p ON oi.product_id = p.id
<where>
o.del_flag = '0'
<if test="orderNo != null and orderNo != ''">
AND o.order_no like concat('%', #{orderNo}, '%')
AND o.order_no = #{orderNo}
</if>
<if test="storeId != null">
AND o.store_id = #{storeId}
@ -82,9 +129,56 @@
<if test="payStatus != null">
AND o.pay_status = #{payStatus}
</if>
<if test="productBarCode != null and productBarCode != ''">
AND p.product_bar_code = #{productBarCode}
</if>
</where>
ORDER BY o.create_time DESC, oi.create_time ASC
ORDER BY o.create_time DESC
</select>
<resultMap id="OrderWithItemsAndProductResultMap" type="com.ruoyi.web.domain.Order">
<id property="id" column="id"/>
<result property="orderNo" column="order_no"/>
<result property="storeId" column="store_id"/>
<result property="payAmount" column="pay_amount"/>
<result property="payStatus" column="pay_status"/>
<result property="payTime" column="pay_time"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<collection property="orderItems" ofType="com.ruoyi.web.domain.OrderItem">
<id property="id" column="item_id"/>
<result property="orderId" column="item_order_id"/>
<result property="productId" column="item_product_id"/>
<result property="productBarCode" column="product_bar_code"/>
<result property="productName" column="item_product_name"/>
<result property="mainImage" column="item_main_image"/>
<result property="quantity" column="item_quantity"/>
<result property="subtotal" column="item_subtotal"/>
</collection>
</resultMap>
<select id="selectForUpdateByOrderNo" parameterType="Order" resultMap="OrderResult">
SELECT *
FROM mall_order
WHERE order_no = #{ordNo}
AND del_flag = '0' FOR UPDATE
</select>
<select id="selectByOrderId" parameterType="OrderItem" resultMap="OrderItemResult">
SELECT *
FROM mall_order_item
WHERE order_id = #{orderId}
AND del_flag = '0'
ORDER BY id ASC
</select>
<select id="getOrderCount" resultType="com.ruoyi.web.vo.OrderCountVO">
select
count(1) as paidOrderCount,
ifnull(sum(pay_amount), 0) as totalPayAmount
from mall_order
where del_flag = '0'
and pay_status = 2 and store_id = #{storeId}
</select>
</mapper>

View File

@ -73,6 +73,21 @@
<if test="status != null and status != '' ">
AND status = #{status}
</if>
<!-- 临期商品筛选(根据传入的临期天数参数) -->
<if test="isExpiring != null and isExpiring == true">
AND production_date IS NOT NULL
AND shelf_life IS NOT NULL
AND DATEDIFF(
DATE_ADD(production_date, INTERVAL shelf_life DAY),
NOW()
) BETWEEN 1 AND approaching
</if>
<!-- 过期商品筛选 -->
<if test="isExpired != null and isExpired == true">
AND production_date IS NOT NULL
AND shelf_life IS NOT NULL
AND DATE_ADD(production_date, INTERVAL shelf_life DAY) &lt; NOW()
</if>
</where>
</select>
@ -90,6 +105,28 @@
where id=#{id} and del_flag = '0'
</select>
<select id="selectBatchForUpdate" parameterType="Product" resultMap="MallProductResult">
SELECT * FROM mall_product_store
WHERE store_id = #{storeId} and del_flag = 0
AND id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">
#{id}
</foreach>
FOR UPDATE
</select>
<select id="getByBarcodes" parameterType="com.ruoyi.web.dto.BatchBarcodeQueryDTO" resultMap="MallProductResult">
SELECT * FROM mall_product_store
WHERE del_flag = '0'
<if test="storeId != null">
AND store_id = #{storeId}
</if>
AND product_bar_code IN
<foreach item="barcode" collection="barcodes" open="(" separator="," close=")">
#{barcode}
</foreach>
</select>
<insert id="insertProductStore" parameterType="Product">
insert into mall_product_store (
@ -134,7 +171,16 @@
sysdate()
)
</insert>
<!-- 增加库存用于回滚带门店ID -->
<update id="addStock">
UPDATE mall_product_store
SET stock_quantity = stock_quantity + #{quantity},
sold_quantity = sold_quantity - #{quantity},
update_time = NOW()
WHERE store_id = #{storeId}
AND id = #{productId}
AND del_flag = 0
</update>
<update id="updateProduct">
update mall_product_store
@ -188,6 +234,17 @@
where product_bar_code = #{productBarCode}
</update>
<update id="reduceStock">
UPDATE mall_product_store
SET stock_quantity = stock_quantity - #{quantity}, -- 扣减库存
sold_quantity = sold_quantity + #{quantity}, -- 增加销量
update_time = NOW() -- 更新时间
WHERE store_id = #{storeId}
AND id = #{productId}
AND stock_quantity >= #{quantity} -- 确保库存充足
AND del_flag = 0 -- 未删除
</update>
<delete id="deleteConfigById" parameterType="Long">
delete
from sys_config

View File

@ -11,6 +11,7 @@
<result property="storeId" column="store_id"/>
<result property="storeCode" column="store_code"/>
</collection>
</resultMap>
<resultMap id="userStoresMap" type="com.ruoyi.web.vo.StoreVo">