Compare commits

...

8 Commits

Author SHA1 Message Date
dzq 14b32f6d07 feat(订单): 添加手机号码、内部用户标识和企业微信用户ID字段
为了支持更多用户信息的存储和识别,新增了手机号码、内部用户标识和企业微信用户ID字段。这些字段将用于订单管理和用户身份验证。
2025-04-12 11:17:54 +08:00
dzq 17f5807af6 refactor(api): 优化接口响应处理逻辑和返回类型
- 移除`checkApiResponse`方法中不必要的状态码检查
- 统一修改`WxLoginController`中接口返回类型为`OutputData`,简化响应处理逻辑
2025-04-11 16:38:43 +08:00
dzq e18463279b feat(微信登录): 新增微信登录相关接口及文档
添加微信登录控制器,实现获取微信登录二维码、临时token、短信验证码发送与验证、用户退出登录等功能。同时新增相关接口文档,详细描述各接口的请求与响应格式。
2025-04-11 11:01:05 +08:00
dzq 7e06c9f73f fix(订单): 修复退款逻辑和支付方式设置
修复订单退款逻辑,增加对支付方式的判断,避免对余额支付的订单进行退款操作。同时,优化退款异常处理,返回更详细的错误信息。在创建订单时,增加支付方式的设置。
2025-04-10 10:31:40 +08:00
dzq 38cb4a1595 refactor(approval): 在查询中添加按approval_id降序排序
为了确保返回的审批记录按approval_id降序排列,在SearchApiReturnApprovalQuery的查询条件中添加了orderByDesc("ra.approval_id")。
2025-04-10 08:18:41 +08:00
dzq 92ba2b93a2 feat(审批): 添加审批状态验证和审批处理逻辑
添加审批状态验证方法`validateApprovalStatus`,确保审批状态合法。在`ReturnApprovalApplicationService`中实现审批通过和驳回的逻辑,包括退款金额验证、退款操作以及更新订单商品状态。同时,修复SQL查询中的空格问题。
2025-04-09 16:41:36 +08:00
dzq 980d279ba9 feat(审批): 添加带商品信息的审批列表查询功能
为了在审批列表中展示商品名称和封面图,新增了`selectApprovalWithGoodsInfo`方法,并在相关实体类中添加了`goodsName`和`coverImg`字段。同时,更新了数据库表结构和查询逻辑,确保审批列表能正确返回商品信息。
2025-04-09 10:16:27 +08:00
dzq 6f2eaf92a8 refactor(支付): 将支付相关URL提取到常量类中
将支付、退款等URL从硬编码改为使用常量类PayApiConstants中的常量,提高代码可维护性和可读性。同时修复退款请求参数的编码问题,确保参数正确传递。
2025-04-08 17:56:37 +08:00
22 changed files with 939 additions and 24 deletions

View File

@ -1,12 +1,16 @@
package com.agileboot.api.controller;
import com.agileboot.common.core.dto.ResponseDTO;
import com.agileboot.common.core.page.PageDTO;
import com.agileboot.common.exception.ApiException;
import com.agileboot.common.exception.error.ErrorCode;
import com.agileboot.domain.shop.approval.ReturnApprovalApplicationService;
import com.agileboot.domain.shop.approval.command.AddReturnApprovalCommand;
import com.agileboot.domain.shop.approval.command.UpdateReturnApprovalCommand;
import com.agileboot.domain.shop.approval.db.ReturnApprovalEntity;
import com.agileboot.domain.shop.approval.model.ReturnApprovalModel;
import com.agileboot.domain.shop.approval.query.SearchApiReturnApprovalQuery;
import com.agileboot.domain.shop.approval.query.SearchReturnApprovalQuery;
import com.agileboot.domain.shop.order.OrderApplicationService;
import com.agileboot.domain.shop.order.db.ShopOrderGoodsEntity;
@ -14,14 +18,11 @@ import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.*;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.math.BigDecimal;
import java.util.Date;
/**
@ -37,13 +38,48 @@ import java.util.Date;
@Slf4j
@Api(tags = "审批接口")
public class ApprovalApiController {
/** 退货审批应用服务,用于处理审批相关业务逻辑 */
private final ReturnApprovalApplicationService approvalApplicationService;
/** 订单应用服务,用于处理订单相关查询操作 */
private final OrderApplicationService orderApplicationService;
/**
* 处理审批操作
*
* @param command 包含审批操作信息的命令对象
* @return 操作结果响应
* @throws ApiException 当参数校验失败或业务逻辑出错时抛出
*/
@PostMapping("/handle")
@ApiOperation(value = "处理审批操作")
public ResponseDTO<String> handleApproval(@Valid @RequestBody UpdateReturnApprovalCommand command) {
if (command.getApprovalId() == null) {
return ResponseDTO.fail(new ApiException(ErrorCode.Internal.INTERNAL_ERROR, "审批ID不能为空"));
}
if (command.getStatus() == null) {
return ResponseDTO.fail(new ApiException(ErrorCode.Internal.INTERNAL_ERROR, "操作状态不能为空"));
}
try {
if (command.getStatus() == 2) {
if (command.getReturnAmount() == null || command.getReturnAmount().compareTo(BigDecimal.ZERO) <= 0) {
return ResponseDTO.fail(new ApiException(ErrorCode.Internal.INTERNAL_ERROR, "归还金额不能为空或小于等于0"));
}
approvalApplicationService.approveApproval(command);
} else if (command.getStatus() == 3) {
approvalApplicationService.rejectApproval(command);
} else {
return ResponseDTO.fail(new ApiException(ErrorCode.Internal.INTERNAL_ERROR, "无效的操作状态"));
}
return ResponseDTO.ok("操作成功");
} catch (Exception e) {
log.error("审批操作失败", e);
return ResponseDTO.fail(new ApiException(ErrorCode.Internal.INTERNAL_ERROR, e.getMessage()));
}
}
/**
* 提交退货审批申请
*
@ -74,4 +110,11 @@ public class ApprovalApiController {
ReturnApprovalEntity returnApproval = approvalApplicationService.submitApproval(command, orderGoods);
return ResponseDTO.ok(returnApproval);
}
@GetMapping("/list")
@ApiOperation(value = "审批列表")
public ResponseDTO<PageDTO<ReturnApprovalEntity>> list(SearchApiReturnApprovalQuery<ReturnApprovalEntity> query) {
PageDTO<ReturnApprovalEntity> page = approvalApplicationService.getApprovalWithGoodsInfo(query);
return ResponseDTO.ok(page);
}
}

View File

@ -20,9 +20,8 @@ import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 调度日志操作处理
*
* @author valarchie
* 订单控制器处理与订单相关的HTTP请求
* 包括订单提交开柜门查询订单和退款等操作
*/
@Slf4j
@RequiredArgsConstructor
@ -33,25 +32,48 @@ public class OrderController extends BaseController {
private final OrderApplicationService orderApplicationService;
private final PaymentApplicationService paymentApplicationService;
// 新增提交订单接口
/**
* 提交订单接口
* @param command 提交订单的请求参数
* @return 包含订单创建结果的响应
*/
@PostMapping("/submit")
public ResponseDTO<CreateOrderResult> submitOrder(@Validated @RequestBody SubmitOrderCommand command) {
CreateOrderResult result = orderApplicationService.createOrder(command);
return ResponseDTO.ok(result);
}
/**
* 打开订单商品柜门接口
* @param orderId 订单ID
* @param orderGoodsId 订单商品ID
* @return 空响应
*/
@PostMapping("/openCabinet/{orderId}/{orderGoodsId}")
public ResponseDTO<?> openCabinet(@PathVariable Long orderId, @PathVariable Long orderGoodsId) {
orderApplicationService.openOrderGoodsCabinet(orderId, orderGoodsId);
return ResponseDTO.ok();
}
/**
* 根据用户openid获取订单列表
* @param openid 用户微信openid
* @return 包含订单列表的响应
*/
@GetMapping("/user/{openid}")
public ResponseDTO<GetOrdersByOpenIdDTO> getOrdersByOpenId(@PathVariable String openid) {
GetOrdersByOpenIdDTO result = orderApplicationService.getOrdersByOpenId(openid);
return ResponseDTO.ok(result);
}
/**
* 订单退款接口
* @param orderId 订单ID
* @param money 退款金额(单位)
* @return 包含退款结果的响应
* @throws IllegalArgumentException 当退款金额为负数或超过订单总额时抛出
* @throws RuntimeException 当退款过程中发生其他错误时抛出
*/
@PostMapping("/refund/{orderId}")
public ResponseDTO<RefundVO> refundOrder(@PathVariable Long orderId, @RequestParam int money) {
OrderModel orderModel = orderApplicationService.loadById(orderId);
@ -74,7 +96,10 @@ public class OrderController extends BaseController {
log.info("退款结果:{}", refundVO);
return ResponseDTO.ok(refundVO);
} catch (Exception e) {
throw new RuntimeException(e);
RefundVO refundVO = new RefundVO();
refundVO.setSuccess(false);
refundVO.setMsg(e.getMessage());
return ResponseDTO.fail(refundVO);
}
}
}

View File

@ -0,0 +1,94 @@
package com.agileboot.api.controller;
import com.agileboot.common.core.dto.ResponseDTO;
import com.agileboot.common.exception.ApiException;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import com.agileboot.domain.ab98.api.Ab98ApiUtil;
import javax.validation.constraints.NotBlank;
import java.util.Map;
@RestController
@RequestMapping("/api/wx/login")
@CrossOrigin(origins = "*", allowedHeaders = "*")
@RequiredArgsConstructor
@Api(tags = "微信登录接口")
public class WxLoginController {
@PostMapping("/logout")
@ApiOperation(value = "用户退出登录")
public ResponseDTO<Ab98ApiUtil.LogoutData> logout(@RequestParam @NotBlank String token) {
try {
return ResponseDTO.ok(Ab98ApiUtil.doLogout(token).getOutputData());
} catch (ApiException e) {
return ResponseDTO.fail(e);
}
}
/**
* 获取微信登录二维码链接
*/
@GetMapping("/wechat/qrcode")
public ResponseDTO<String> getWechatQrCode(@RequestParam @NotBlank String token) {
try {
return ResponseDTO.ok(Ab98ApiUtil.generateWechatLoginUrl(token));
} catch (ApiException e) {
return ResponseDTO.fail(e);
}
}
/**
* 获取临时token
*/
@GetMapping("/getToken")
@ApiOperation(value = "获取临时令牌", notes = "用于后续登录流程")
public ResponseDTO<Ab98ApiUtil.TokenData> getToken(@RequestParam String appName) {
try {
return ResponseDTO.ok(Ab98ApiUtil.getToken(appName).getOutputData());
} catch (ApiException e) {
return ResponseDTO.fail(e);
}
}
/**
* 发送短信验证码
*/
@PostMapping("/sendSms")
public ResponseDTO<Ab98ApiUtil.SmsResult> sendSms(
@RequestParam String token,
@RequestParam String tel) {
try {
return ResponseDTO.ok(Ab98ApiUtil.sendLoginSms(token, tel).getOutputData());
} catch (ApiException e) {
return ResponseDTO.fail(e);
}
}
/**
* 验证短信验证码
*/
@PostMapping("/verifySms")
public ResponseDTO<Ab98ApiUtil.LoginData> verifySms(
@RequestParam String token,
@RequestParam String tel,
@RequestParam String vcode) {
try {
Ab98ApiUtil.LoginResponse loginResponse = Ab98ApiUtil.verifySmsCode(token, tel, vcode);
Ab98ApiUtil.LoginData data = new Ab98ApiUtil.LoginData();
data.setFace_img(loginResponse.getOutputData().getFace_img());
data.setSuccess(loginResponse.getOutputData().isSuccess());
data.setSex(loginResponse.getOutputData().getSex());
data.setName(loginResponse.getOutputData().getName());
data.setUserid(loginResponse.getOutputData().getUserid());
data.setRegistered(loginResponse.getOutputData().isRegistered());
data.setTel(loginResponse.getOutputData().getTel());
return ResponseDTO.ok(data);
} catch (ApiException e) {
return ResponseDTO.fail(e);
}
}
}

View File

@ -3,5 +3,8 @@ package com.agileboot.common.constant;
public class PayApiConstants {
public static final String biz_id = "wxshop";
public static final String appkey = "wxshop202503081132";
public static final String pay_url = "http://222.218.10.217:7890/open/trade/wx/jsapi/precreate";
public static final String pay_callback_url = "http://wxshop.ab98.cn/shop-api/api/payment/callback";
public static final String refund_url = "http://222.218.10.217:7890/open/trade/refund";
public static final String refund_callback_url = "http://wxshop.ab98.cn/shop-api/api/payment/refund/callback";
}

View File

@ -0,0 +1,234 @@
package com.agileboot.domain.ab98.api;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.agileboot.common.exception.ApiException;
import com.agileboot.common.exception.error.ErrorCode;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class Ab98ApiUtil {
private static final String BASE_URL = "https://www.ab98.cn/api/doInterface";
private static final String WEBSOCKET_URL = "wss://www.ab98.cn/login.ws/";
/**
* 短信登录发送验证码
*/
/**
* 发送短信验证码短信登录
* @param token 通过getToken获取的临时令牌有效期5分钟
* @param tel 接收验证码的手机号码需符合格式11位数字
* @param nobind "true"表示不绑定手机号固定值
* @param for_login "true"表示用于登录固定值
* @param from 来源渠道固定值"jt"
*/
public static SmsSendResponse sendLoginSms(String token, String tel) {
String url = BASE_URL + "?code=doSendSms";
Map<String, Object> paramMap = new HashMap<String, Object>() {{
put("token", token);
put("tel", tel);
put("nobind", "true");
put("for_login", "true");
put("from", "jt");
}};
String response = HttpUtil.createPost(url)
.body(JSONUtil.toJsonStr(paramMap))
.header("noSign", "true")
.header("source", "api")
.execute().body();
log.info("短信发送响应: {}", response);
SmsSendResponse resp = JSONUtil.toBean(response, SmsSendResponse.class);
checkApiResponse(resp);
return resp;
}
/**
* 验证短信验证码
*/
/**
* 验证短信验证码短信登录
* @param token 通过getToken获取的临时令牌
* @param tel 接收验证码的手机号码
* @param vcode 用户输入的6位验证码有效期5分钟
* @return 登录结果包含用户身份信息
* @throws ApiException 当验证失败或接口返回异常时抛出
*/
public static LoginResponse verifySmsCode(String token, String tel, String vcode) {
String url = BASE_URL + "?code=doCheckSmsCode";
Map<String, Object> paramMap = new HashMap<String, Object>() {{
put("token", token);
put("tel", tel);
put("vcode", vcode);
}};
String response = HttpUtil.createPost(url)
.body(JSONUtil.toJsonStr(paramMap))
.header("noSign", "true")
.header("source", "api")
.execute().body();
log.info("短信验证响应: {}", response);
LoginResponse resp = JSONUtil.toBean(response, LoginResponse.class);
checkApiResponse(resp);
return handleLoginResult(resp);
}
/**
* 处理登录结果
*/
private static LoginResponse handleLoginResult(LoginResponse response) {
if (response.getOutputData() == null || !response.getOutputData().isSuccess()) {
log.error("登录失败: {}", response);
throw new ApiException(ErrorCode.FAILED, "登录验证失败");
}
return response;
}
/**
* 生成微信扫码登录URL
*/
public static String generateWechatLoginUrl(String token) {
return "https://www.ab98.cn/online/index.html?content=doLogin%60" + token;
}
/**
* 监听扫码登录websocket
*/
/*public static void listenLoginWebsocket(String token, WebSocketListener listener) {
String url = WEBSOCKET_URL + token;
HttpUtil.createWebSocket(url, listener).connect();
}*/
/**
* 获取登录token
*/
/**
* 获取登录token
* @param appName 应用英文名需在汇邦数字平台登记
* @return outputData.token 临时令牌后续接口需携带
* @return outputData.takeFace 是否需要人脸验证true需要刷脸
*/
public static TokenResponse getToken(String appName) {
// String url = BASE_URL + "?code=doGetToken&from=jt&app=" + appName;
String url = BASE_URL + "?code=doGetToken&from=jt";
String response = HttpUtil.createGet(url)
.header("noSign", "true")
.header("source", "api")
.execute().body();
log.info("获取token响应: {}", response);
TokenResponse tokenResponse = JSONUtil.toBean(response, TokenResponse.class);
checkApiResponse(tokenResponse);
return tokenResponse;
}
/**
* 构建带参数的请求URL
*/
/**
* 检查接口响应状态
*/
private static void checkApiResponse(BaseResponse response) {
if (!"ok".equals(response.getState())) {
log.error("接口调用失败: {}", response);
throw new ApiException(ErrorCode.FAILED, "第三方接口调用失败");
}
}
// 基础响应对象
@Data
public static class BaseResponse {
private String state;
private Integer stateCode;
private Object outputData;
}
@Data
public static class SmsSendResponse extends BaseResponse {
private SmsResult outputData;
}
@Data
public static class SmsResult {
private boolean success;
private String errMsg;
}
@Data
public static class LoginResponse extends BaseResponse {
private LoginData outputData;
}
@Data
public static class LoginData {
private String idcard_back; // 身份证背面照片地址
private String face_img; // 人脸照片地址
private String address; // 身份证登记地址
private String nation; // 民族接口文档显示可能为null
private boolean success; // 验证是否成功
private String sex; // 性别/
private String name; // 真实姓名
private String userid; // 用户在平台的唯一ID
private boolean registered; // 是否已注册
private String tel; // 手机号码
private String idnum; // 身份证号码
private String idcard_front; // 身份证正面照片地址
}
/**
* 用户退出接口
*/
public static LogoutResponse doLogout(String token) {
String url = BASE_URL + "?code=doLogout";
Map<String, Object> paramMap = new HashMap<String, Object>() {{
put("token", token);
}};
String response = HttpUtil.createPost(url)
.body(JSONUtil.toJsonStr(paramMap))
.header("noSign", "true")
.header("source", "api")
.execute().body();
log.info("用户退出响应: {}", response);
LogoutResponse resp = JSONUtil.toBean(response, LogoutResponse.class);
checkApiResponse(resp);
return resp;
}
@Data
public static class LogoutResponse extends BaseResponse {
private LogoutData outputData; // 包含success和logoutTime登出时间
}
@Data
public static class LogoutData {
private boolean success;
private String logoutTime;
}
// Token响应对象
@Data
public static class TokenResponse extends BaseResponse {
private TokenData outputData;
}
@Data
public static class TokenData {
private String token;
private boolean takeFace;
}
}

View File

@ -1,5 +1,6 @@
package com.agileboot.domain.shop.approval;
import com.agileboot.common.constant.PayApiConstants;
import com.agileboot.common.core.page.PageDTO;
import com.agileboot.domain.common.command.BulkOperationCommand;
import com.agileboot.domain.shop.approval.command.AddReturnApprovalCommand;
@ -9,19 +10,31 @@ import com.agileboot.domain.shop.approval.db.ReturnApprovalService;
import com.agileboot.domain.shop.approval.dto.ReturnApprovalDTO;
import com.agileboot.domain.shop.approval.model.ReturnApprovalModel;
import com.agileboot.domain.shop.approval.model.ReturnApprovalModelFactory;
import com.agileboot.domain.shop.approval.query.SearchApiReturnApprovalQuery;
import com.agileboot.domain.shop.approval.query.SearchReturnApprovalQuery;
import com.agileboot.domain.shop.order.db.ShopOrderGoodsEntity;
import com.agileboot.domain.shop.order.model.OrderGoodsModel;
import com.agileboot.domain.shop.order.model.OrderGoodsModelFactory;
import com.agileboot.domain.shop.order.model.OrderModel;
import com.agileboot.domain.shop.order.model.OrderModelFactory;
import com.agileboot.domain.shop.payment.PaymentApplicationService;
import com.agileboot.domain.shop.payment.dto.RefundVO;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.stereotype.Service;
/**
* 退货审批应用服务类处理与退货审批相关的业务逻辑
*/
@Service
@Slf4j
@RequiredArgsConstructor
@ -30,7 +43,14 @@ public class ReturnApprovalApplicationService {
private final ReturnApprovalService approvalService;
private final ReturnApprovalModelFactory modelFactory;
private final OrderGoodsModelFactory orderGoodsModelFactory;
private final OrderModelFactory orderModelFactory;
private final PaymentApplicationService paymentApplicationService;
/**
* 获取退货审批列表
* @param query 查询条件
* @return 分页的退货审批DTO列表
*/
public PageDTO<ReturnApprovalDTO> getApprovalList(SearchReturnApprovalQuery<ReturnApprovalEntity> query) {
Page<ReturnApprovalEntity> page = approvalService.getApprovalList(query);
List<ReturnApprovalDTO> dtoList = page.getRecords().stream()
@ -39,6 +59,11 @@ public class ReturnApprovalApplicationService {
return new PageDTO<>(dtoList, page.getTotal());
}
/**
* 添加退货审批
* @param command 添加退货审批命令
* @return 创建的退货审批模型
*/
public ReturnApprovalModel addApproval(AddReturnApprovalCommand command) {
ReturnApprovalModel model = modelFactory.create();
model.loadAddCommand(command);
@ -46,12 +71,20 @@ public class ReturnApprovalApplicationService {
return model;
}
/**
* 更新退货审批信息
* @param command 更新退货审批命令
*/
public void updateApproval(UpdateReturnApprovalCommand command) {
ReturnApprovalModel model = modelFactory.loadById(command.getApprovalId());
model.loadUpdateCommand(command);
model.updateById();
}
/**
* 批量删除退货审批
* @param command 批量操作命令包含要删除的审批ID列表
*/
public void deleteApproval(BulkOperationCommand<Long> command) {
for (Long approvalId : command.getIds()) {
ReturnApprovalModel model = modelFactory.loadById(approvalId);
@ -59,11 +92,74 @@ public class ReturnApprovalApplicationService {
}
}
/**
* 审批通过退货申请
* @param command 更新退货审批命令
* @throws IllegalArgumentException 如果退款金额不合法
* @throws RuntimeException 如果退款操作失败
*/
public void approveApproval(UpdateReturnApprovalCommand command) {
ReturnApprovalModel model = modelFactory.loadById(command.getApprovalId());
OrderModel orderModel = orderModelFactory.loadById(model.getOrderId());
OrderGoodsModel orderGoodsModel = orderGoodsModelFactory.loadById(model.getOrderGoodsId());
// 退款金额对比, 退款金额不能大于订单金额
// 金额转换元转分并四舍五入
BigDecimal amountInFen = orderGoodsModel.getTotalAmount()
.multiply(new BigDecimal("100"))
.setScale(0, RoundingMode.HALF_UP);
BigDecimal returnAmount = command.getReturnAmount();
// 退款金额转换元转分并四舍五入
returnAmount = returnAmount
.multiply(new BigDecimal("100"))
.setScale(0, RoundingMode.HALF_UP);
if (returnAmount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("退款金额必须大于0");
}
if (returnAmount.compareTo(amountInFen) > 0) {
throw new IllegalArgumentException("退款金额不能超过订单总额");
}
if (!Objects.equals(orderModel.getPaymentMethod(), "balance")) {
RefundVO refundVO = null;
try {
refundVO = paymentApplicationService.refund(
PayApiConstants.biz_id, PayApiConstants.appkey,
orderModel.getBizOrderId(), orderModel.getUcid(),
"退还", returnAmount.intValue());
} catch (Exception e) {
throw new RuntimeException("退款失败", e);
}
log.info("退款结果:{}", refundVO);
if (null == refundVO || !refundVO.getSuccess()) {
throw new RuntimeException("退款失败");
}
}
model.validateApprovalStatus();
model.setAuditImages(command.getAuditImages());
model.setAuditRemark(command.getAuditRemark());
model.setReturnAmount(command.getReturnAmount());
model.setStatus(2); // 2表示审核通过状态
model.updateById();
// 更新关联订单商品状态
orderGoodsModel.setStatus(2); // 6表示已完成退货
orderGoodsModel.updateById();
}
/**
* 提交退货审批申请
* @param command 添加退货审批命令
* @param orderGoods 关联的订单商品实体
* @return 创建的退货审批实体
*/
public ReturnApprovalEntity submitApproval(AddReturnApprovalCommand command, ShopOrderGoodsEntity orderGoods) {
// 设置商品价格并初始化审批状态
command.setGoodsId(orderGoods.getGoodsId());
command.setOrderId(orderGoods.getOrderId());
command.setGoodsPrice(orderGoods.getPrice());
command.setGoodsPrice(orderGoods.getTotalAmount());
command.setReturnImages(command.getReturnImages());
command.setReturnRemark(command.getReturnRemark());
command.setStatus(1);
@ -83,4 +179,24 @@ public class ReturnApprovalApplicationService {
return returnApprovalModel.selectById();
}
public PageDTO<ReturnApprovalEntity> getApprovalWithGoodsInfo(SearchApiReturnApprovalQuery<ReturnApprovalEntity> query) {
Page<ReturnApprovalEntity> page = approvalService.selectApprovalWithGoodsInfo(query);
return new PageDTO<>(page.getRecords(), page.getTotal());
}
/**
* 审批驳回退货申请
* @param command 更新退货审批命令包含驳回原因
*/
public void rejectApproval(UpdateReturnApprovalCommand command) {
ReturnApprovalModel model = modelFactory.loadById(command.getApprovalId());
model.validateApprovalStatus();
// 更新审批状态为驳回
model.setStatus(3);
model.setAuditImages(command.getAuditImages());
model.setAuditRemark(command.getAuditRemark());
model.updateById();
}
}

View File

@ -76,6 +76,14 @@ public class ReturnApprovalEntity extends BaseEntity<ReturnApprovalEntity> {
@TableField("`status`")
private Integer status;
@ApiModelProperty("商品名称")
@TableField(exist = false)
private String goodsName;
@ApiModelProperty("封面图URL")
@TableField(exist = false)
private String coverImg;
@Override
public Serializable pkVal() {

View File

@ -17,6 +17,14 @@ import org.apache.ibatis.annotations.Select;
* @since 2025-04-03
*/
public interface ReturnApprovalMapper extends BaseMapper<ReturnApprovalEntity> {
@Select("SELECT ra.*, sog.goods_name AS goodsName, sog.cover_img AS coverImg " +
"FROM return_approval ra " +
"LEFT JOIN shop_order_goods sog ON ra.order_goods_id = sog.order_goods_id " +
"${ew.customSqlSegment}")
Page<ReturnApprovalEntity> selectApprovalWithGoodsInfo(Page<ReturnApprovalEntity> page,
@Param(Constants.WRAPPER) Wrapper<ReturnApprovalEntity> queryWrapper);
@Select("SELECT * " +
"FROM return_approval " +
"${ew.customSqlSegment}")

View File

@ -21,4 +21,6 @@ public interface ReturnApprovalService extends IService<ReturnApprovalEntity> {
List<ReturnApprovalEntity> selectAll();
ReturnApprovalEntity getByOrderId(Long orderId);
Page<ReturnApprovalEntity> selectApprovalWithGoodsInfo(AbstractPageQuery<ReturnApprovalEntity> query);
}

View File

@ -1,6 +1,7 @@
package com.agileboot.domain.shop.approval.db;
import com.agileboot.common.core.page.AbstractPageQuery;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@ -38,4 +39,9 @@ public class ReturnApprovalServiceImpl extends ServiceImpl<ReturnApprovalMapper,
.last("LIMIT 1");
return this.getOne(wrapper);
}
@Override
public Page<ReturnApprovalEntity> selectApprovalWithGoodsInfo(AbstractPageQuery<ReturnApprovalEntity> query) {
return this.baseMapper.selectApprovalWithGoodsInfo(query.toPage(), query.toQueryWrapper());
}
}

View File

@ -36,4 +36,14 @@ public class ReturnApprovalModel extends ReturnApprovalEntity {
loadAddCommand(command);
}
}
public void validateApprovalStatus() {
if (getStatus() == null || !isValidStatus(getStatus())) {
throw new RuntimeException("审批状态不合法");
}
}
private boolean isValidStatus(Integer status) {
return !status.equals(2);
}
}

View File

@ -0,0 +1,41 @@
package com.agileboot.domain.shop.approval.query;
import cn.hutool.core.util.StrUtil;
import com.agileboot.common.core.page.AbstractPageQuery;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import java.util.Date;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class SearchApiReturnApprovalQuery<T> extends AbstractPageQuery<T> {
private Long approvalId;
private Long orderId;
private Long goodsId;
private String goodsName;
private Integer status;
private String returnRemark;
private String auditRemark;
private Date startTime;
private Date endTime;
@Override
public QueryWrapper<T> addQueryCondition() {
QueryWrapper<T> queryWrapper = new QueryWrapper<>();
queryWrapper
.eq(approvalId != null, "ra.approval_id", approvalId)
.eq(orderId != null, "ra.order_id", orderId)
.eq(goodsId != null, "ra.goods_id", goodsId)
.eq(status != null, "ra.status", status)
.like(StrUtil.isNotEmpty(goodsName), "sog.goods_name", goodsName)
.between(startTime != null && endTime != null, "ra.create_time", startTime, endTime)
.orderByDesc("ra.approval_id");
this.timeRangeColumn = "create_time";
return queryWrapper;
}
}

View File

@ -127,6 +127,10 @@ public class OrderApplicationService {
orderModel.setStatus(1);
orderModel.generateOrderNumber();
orderModel.setTotalAmount(BigDecimal.valueOf(0));
orderModel.setPaymentMethod(command.getPaymentType());
orderModel.setMobile(command.getMobile());
orderModel.setIsInternal(command.getIsInternal());
orderModel.setUserid(command.getQyUserid());
orderModel.insert();
processOrderGoods(orderModel, goodsList);
@ -169,7 +173,7 @@ public class OrderApplicationService {
request.setPay_amount(amountInFen.toPlainString()); // 单位转换为分
request.setTitle("商品订单支付");
request.setNotify_url("http://wxshop.ab98.cn/shop-api/api/payment/callback");
request.setNotify_url(PayApiConstants.pay_callback_url);
request.setBiz_id(PayApiConstants.biz_id);
request.setUcid(orderModel.getUcid());
request.setExtra("");

View File

@ -3,15 +3,34 @@ package com.agileboot.domain.shop.order.command;
import com.agileboot.domain.shop.order.db.ShopOrderEntity;
import com.agileboot.domain.shop.order.db.ShopOrderGoodsEntity;
import java.util.List;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
public class SubmitOrderCommand {
@ApiModelProperty("微信用户唯一标识")
private String openid;
@ApiModelProperty("系统用户ID")
private String userid;
@ApiModelProperty("企业ID")
private String corpid;
@ApiModelProperty("订单主体信息")
private ShopOrderEntity order;
// 支付类型'wechat' | 'balance'
@ApiModelProperty("支付类型 wechat:微信 balance:余额")
private String paymentType;
@ApiModelProperty("订单商品明细列表")
private List<ShopOrderGoodsEntity> goodsList;
@ApiModelProperty("联系电话")
private String mobile;
@ApiModelProperty("企业微信用户ID或汇邦云用户ID")
private String qyUserid;
@ApiModelProperty("是否内部订单 0否 1汇邦云用户 2企业微信用户")
private Integer isInternal;
}

View File

@ -45,6 +45,18 @@ public class ShopOrderEntity extends BaseEntity<ShopOrderEntity> {
@TableField("trade_id")
private String tradeId;
@ApiModelProperty("手机号码")
@TableField("mobile")
private String mobile;
@ApiModelProperty("企业微信用户ID或汇邦云用户ID")
@TableField("userid")
private String userid;
@ApiModelProperty("是否内部用户0否 1汇邦云用户 2企业微信用户")
@TableField("is_internal")
private Integer isInternal;
@ApiModelProperty("业务系统订单ID对接外部系统")
@TableField("biz_order_id")
private String bizOrderId;

View File

@ -52,6 +52,14 @@ public class ShopOrderGoodsEntity extends BaseEntity<ShopOrderGoodsEntity> {
@TableField("total_amount")
private BigDecimal totalAmount;
@ApiModelProperty("商品名称")
@TableField("goods_name")
private String goodsName;
@ApiModelProperty("封面图URL")
@TableField("cover_img")
private String coverImg;
@ApiModelProperty("商品状态1正常 2已退货 3已换货 4已完成 5审核中 6退货未通过")
@TableField("`status`")
private Integer status;

View File

@ -1,12 +1,16 @@
package com.agileboot.domain.shop.order.dto;
import cn.hutool.core.bean.BeanUtil;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import com.agileboot.domain.shop.order.db.ShopOrderEntity;
import java.math.BigDecimal;
import java.util.Date;
import lombok.Data;
@Data
@ApiModel(value = "ShopOrderDTO对象", description = "商品订单DTO")
public class ShopOrderDTO {
public ShopOrderDTO(ShopOrderEntity entity) {
@ -23,14 +27,29 @@ public class ShopOrderDTO {
}
}
@ApiModelProperty("订单唯一ID")
private Long orderId;
@ApiModelProperty("ucid")
private String ucid;
@ApiModelProperty("openid")
private String openid;
@ApiModelProperty("支付网关交易id")
private String tradeId;
@ApiModelProperty("订单总金额")
private BigDecimal totalAmount;
@ApiModelProperty("订单状态1待付款 2已付款 3已发货 4已完成 5已取消")
private Integer status;
@ApiModelProperty("支付状态1未支付 2已支付 3退款中 4已退款")
private Integer payStatus;
@ApiModelProperty("支付方式")
private String paymentMethod;
@ApiModelProperty("支付时间")
private Date payTime;
private Date createTime;
@ApiModelProperty("手机号码")
private String mobile;
@ApiModelProperty("是否内部用户0否 1是")
private Integer isInternal;
@ApiModelProperty("业务系统订单ID对接外部系统")
private String bizOrderId;
}

View File

@ -35,6 +35,8 @@ public class OrderGoodsModel extends ShopOrderGoodsEntity {
ShopGoodsEntity goods = goodsService.getById(getGoodsId());
if (goods != null) {
BigDecimal price = goods.getPrice();
this.setGoodsName(goods.getGoodsName());
this.setCoverImg(goods.getCoverImg());
this.setPrice(price);
this.setTotalAmount(price.multiply(BigDecimal.valueOf(getQuantity())));
}

View File

@ -1,5 +1,7 @@
package com.agileboot.domain.shop.order.model;
import com.agileboot.common.exception.ApiException;
import com.agileboot.common.exception.error.ErrorCode;
import com.agileboot.domain.shop.goods.db.ShopGoodsService;
import com.agileboot.domain.shop.order.db.ShopOrderGoodsEntity;
import com.agileboot.domain.shop.order.db.ShopOrderGoodsService;
@ -13,6 +15,14 @@ public class OrderGoodsModelFactory {
private final ShopOrderGoodsService orderGoodsService;
private final ShopGoodsService goodsService;
public OrderGoodsModel loadById(Long orderGoodsId) {
ShopOrderGoodsEntity entity = orderGoodsService.getById(orderGoodsId);
if (entity == null) {
throw new ApiException(ErrorCode.Business.COMMON_OBJECT_NOT_FOUND, orderGoodsId, "订单商品");
}
return new OrderGoodsModel(entity, orderGoodsService, goodsService);
}
public OrderGoodsModel create(ShopOrderGoodsEntity entity) {
return new OrderGoodsModel(entity, orderGoodsService, goodsService);
}

View File

@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
@ -34,7 +35,7 @@ public class PaymentApplicationService {
private static final Object LOCKER = new Object();
public WxJsApiPreCreateResponse callJsApiPreCreate(WxJsApiPreCreateRequest request) {
String gatewayUrl = "http://222.218.10.217:7890/open/trade/wx/jsapi/precreate";
String gatewayUrl = PayApiConstants.pay_url;
try {
String jsonBody = JSONUtil.toJsonStr(request);
@ -115,7 +116,7 @@ public class PaymentApplicationService {
* @param money 退款金额单位
*/
public RefundVO refund(String bizId, String appKey, String orderId, String uid, String reason, int money) throws Exception {
String url = "http://222.218.10.217:7890/open/trade/refund";
String url = PayApiConstants.refund_url;
JSONObject bizContent = new JSONObject();
bizContent.set("userId", uid);
bizContent.set("bizId", bizId);
@ -129,7 +130,8 @@ public class PaymentApplicationService {
params.put("timestamp", String.valueOf(System.currentTimeMillis()));
params.put("version", "1.0");
params.put("nonce_str", UUID.randomUUID().toString());
params.put("biz_content", URLUtil.encode(JSONUtil.toJsonStr(bizContent)));
params.put("biz_content", URLEncoder.encode(JSONUtil.toJsonStr(bizContent), StandardCharsets.UTF_8.name()));
log.info("退款请求参数:{}", JSONUtil.toJsonStr(params));
params.put("sign", SignUtils.openSign(appKey, params));
StringBuilder sb = new StringBuilder();
for (String key : params.keySet()) {
@ -202,8 +204,8 @@ public class PaymentApplicationService {
String orderNO = bizContent.getBizOrderId();
if (StringUtils.isBlank(orderNO)) {
res.put("callback_code", 1);
res.put("callback_msg", "缺少退款订单ID");
res.set("callback_code", 1);
res.set("callback_msg", "缺少退款订单ID");
return JSONUtil.toJsonStr(res);
}
synchronized (LOCKER) {
@ -212,8 +214,9 @@ public class PaymentApplicationService {
if (orderNO.startsWith("wxshop-")) {
// 订单号格式为 wxshop-1-time提取中间的订单号
String orderId = orderNO.split("-")[1];
OrderModel orderModel = orderModelFactory.loadById(Long.valueOf(orderId));
orderModel.handleRefundSuccess();
log.info("退款回调处理成功, 订单号:{}", orderId);
// OrderModel orderModel = orderModelFactory.loadById(Long.valueOf(orderId));
// orderModel.handleRefundSuccess();
}
}
res.set("callback_code", 0);

View File

@ -0,0 +1,232 @@
## 请求概述
### 请求header
所有请求必须包含以下两个header
* `noSign`: `true`
* `source`: `api`
获取登录token后也可以在header中添加
* `token`: `${token}`
### 登录校验结果
接口返回结果为json格式如果包含`code`字段,则说明登录校验失败。
* `code`: `0001` - 登录状态已失效
* `code`: `0002` - 在cookie、Get参数、Header中均未检测到token
## 登录接口
### 2.1 获取token
* **API**: `https://www.ab98.cn/api/doInterface?code=doGetToken&from=jt&app=${appname}`
* **请求方式**: GET
* **请求参数**:
* `code`: `doGetToken` (必传)
* `from`: `jt` (可选)
* `app`: 调用该接口的应用在汇邦数字平台登记的应用英文名 (可选)
**成功返回**:
```json
{
"outputData": {
"token": "1a6ea39e84c406283839856640e3aa66",
"takeFace": true
},
"state": "ok"
}
```
**失败返回**:
```json
{
"outputData": "错误信息",
"stateCode": 0,
"state": "fail"
}
```
### 2.2 短信登录
#### 2.2.1 发送短信
* **API**: `https://www.ab98.cn/api/doInterface?code=doSendSms`
* **请求方式**: POST
* **Content-Type**: application/json
* **请求BODY**:
```json
{
"token":"358900e1005c33a1dd059b07042ceec3", //必传
"tel":"137xxxxxxxx", //必传
"nobind":"true", //必传
"for_login":"true", //必传
"from":"jt" //必传
}
```
**成功返回**:
```json
{
"outputData": {
"success": true
},
"state": "ok"
}
```
**失败返回**:
```json
{
"outputData": {
"success": false,
"errMsg": "失败原因"
},
"state": "ok"
}
```
#### 2.2.2 验证短信验证码
* **API**: `https://www.ab98.cn/api/doInterface?code=doCheckSmsCode`
* **请求方式**: POST
* **Content-Type**: application/json
* **请求BODY**:
```json
{
"token":"358900e1005c33a1dd059b07042ceec3",
"tel":"137xxxxxxxx",
"vcode":"123456"
}
```
**成功返回**:
```json
{
"outputData": {
"idcard_back": "https://www.ab98.cn/upload/temp/images/202102/xxx.jpeg",
"face_img": "https://www.ab98.cn/upload/temp/images/202102/yyy.jpeg",
"address": "身份证上的地址",
"nation": null,
"success": true,
"sex": "男",
"name": "xxx",
"userid": 123,
"registered": true,
"tel": "137xxxxxxxx",
"idnum": "450981xxxxxxxxxxxx",
"idcard_front": "https://www.ab98.cn/upload/temp/images/202102/zzz.jpeg"
},
"state": "ok"
}
```
**失败返回**:
```json
{
"outputData": {
"success": false
},
"state": "ok"
}
```
### 2.3 微信扫码登录
* **API**: `https://www.ab98.cn/online/index.html?content=doLogin%60`
#### 2.3.1 二维码
假设获取到的token为`358900e1005c33a1dd059b07042ceec3`,则构造的最终链接为:
`https://www.ab98.cn/online/index.html?content=doLogin%60358900e1005c33a1dd059b07042ceec3`
使用该链接生成二维码图片供用户使用微信“扫一扫”。
#### 2.3.2 监听websocket
* **监听地址**: `wss://www.ab98.cn/login.ws/${token}`
* **登录失败**:
```json
{
"outputData": "登录失败原因",
"state": "fail"
}
```
* **登录成功**:
```json
{
"outputData": "登录成功",
"state": "ok",
"username": "扫码者姓名",
"sex": "扫码者性别",
"head_img": "扫码者头像",
"idnum": "身份证号码",
"userid": "人员记录ID",
"tel": "手机号码"
}
```
### 2.4 刷脸登录
* **API**: `https://www.ab98.cn/api/doInterface?code=doStrongFaceLogin`
* **请求方式**: POST
* **Content-Type**: application/json
* **请求BODY**:
```json
{
"token":"358900e1005c33a1dd059b07042ceec3", //必传
"check_code":"1234", //必传
"imgBase64":"/9j/4AAQS…" //必传
}
```
**成功返回**:
```json
{
"state": "ok",
"outputData": "登录成功",
"openid": "oGfFD1jBEZ2sq4PhOc8zKKejHA9E",
"head_img": "https://www.ab98.cn/… ",
"sex": "男",
"userid": 1,
"token": "dbe11ef64cd6350c5d935d531db765c0",
"username": "xxx",
"tel": "xxx"
}
```
### 2.5 动态码登录
1. **发起websocket监听**:
`wss://www.ab98.cn/login.ws/{监听token}`
其中 `{监听token}` 由客户端自定义,由数字和英文字母组成,保证多用户同时监听时 `{监听token}` 互不相同即可,否则会接收到错误消息。
2. **主动发送消息**:
```json
{
"type": "mfa",
"do": "checkCode",
"token": "xxx",
"code": "000000"
}
```
**服务器响应消息**:
1. **登录失败**:
```json
{
"state": "fail",
"msg": "失败原因"
}
```
2. **动态码校验通过,等待移动端授权登录**:
```json
{
"state": "ok",
"step": "0"
}
```
3. **等待移动端授权超时**:
```json
{
"state": "ok",
"step": "1"
}
```
4. **移动端已授权登录**:
```json
{
"state": "ok",
"step": "2",
"openid": "xxx",
"head_img": "/xxx",
"sex": "男/女",
"company": "xxx",
"userid": 123,
"idnum": "xxx",
"username": "xxx",
"tel": "xxx"
}
```
### 2.6 退出登录
* **API**: `https://www.ab98.cn/api/doInterface?code=doLogout`
* **请求方式**: POST
* **Content-Type**: application/json
* **请求BODY**:
```json
{
"token":"358900e1005c33a1dd059b07042ceec3"
}
```
**成功返回**:
```json
{
"state": "ok"
}
```

View File

@ -49,4 +49,20 @@ ALTER TABLE `return_approval`
ADD CONSTRAINT `fk_return_order_goods` FOREIGN KEY (`order_goods_id`) REFERENCES `shop_order_goods` (`order_goods_id`);
ALTER TABLE `shop_order`
ADD COLUMN `biz_order_id` VARCHAR(32) NULL COMMENT '业务系统订单ID对接外部系统' AFTER `trade_id`;
ADD COLUMN `biz_order_id` VARCHAR(32) NULL COMMENT '业务系统订单ID对接外部系统' AFTER `trade_id`;
ALTER TABLE shop_order_goods
ADD COLUMN goods_name VARCHAR(255) NOT NULL COMMENT '商品名称',
ADD COLUMN cover_img VARCHAR(512) COMMENT '封面图URL';
UPDATE shop_order_goods sog
INNER JOIN shop_goods sg ON sog.goods_id = sg.goods_id
SET sog.goods_name = sg.goods_name,
sog.cover_img = sg.cover_img;
ALTER TABLE `shop_order`
ADD COLUMN `mobile` varchar(30) DEFAULT NULL COMMENT '手机号码' AFTER `trade_id`,
ADD COLUMN `is_internal` TINYINT(1) DEFAULT 0 COMMENT '是否内部用户0否 1是';
ALTER TABLE `shop_order`
ADD COLUMN `userid` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '企业微信用户ID或汇邦云用户ID' AFTER `mobile`;