购物车 → 生成订单 → 支付宝沙箱支付 笔记
〇、全链路总览
【阶段一】商品详情页点"加入购物车"
ProductDetail.addCart() ──POST /api/cart──► 网关(验JWT,透传用户头) ──► mall-cart CartController.add
└► CartServiceImpl.add:有则数量累加,无则新插,user_id=当前登录人
【阶段二】购物车勾选商品,点"去结算"
Cart.vue ──router.push──► OrderConfirm.vue:加载购物车勾选项 + 收货地址
【阶段三】确认订单页点"提交订单"
OrderConfirm.submitOrder() ──POST /api/order {shippingId}──► 网关 ──► mall-order OrderController.add
└► OrderServiceImpl.add:
① 从ThreadLocal取userId
② Feign调cart-service查"已勾选"的购物车项(含商品信息)
③ 算总价 → 插orders表 → 逐条插order_item表 → Feign调cart-service删掉已下单的购物车项
【阶段四】我的订单页,待付款订单点"去支付"
OrderList.pay() ──GET /api/order/pay/{orderNo}──► 网关 ──► OrderController.pay
└► OrderServiceImpl.pay:校验归属+状态 → AlipayServiceImpl.createPayForm:调支付宝沙箱生成表单HTML
前端把form插入页面自动submit ──► 跳转支付宝沙箱收银台 ──► 沙箱买家账号付款
【阶段五】付款完成后
浏览器跳回 returnUrl(localhost:5174/order) → 页面自动主动查单
OrderList.checkPayingOrder() ──GET /api/order/payStatus/{orderNo}──► syncPayStatus:查支付宝真实状态→已支付则流转状态
(生产环境另有支付宝服务器POST /order/notify 异步通知,验签后流转状态;本地收不到,见"联调限制")
贯穿全程的两条暗线:
- 用户身份链路:JWT 存在前端 localStorage → 每次请求带
Authorization头 → 网关AuthGlobalFilter验签后把用户 id/name 写进X-Login-Id/X-Login-Name请求头 → 各服务的LoginInterceptor读出头存入 ThreadLocal(LoginContext)→ 业务代码随时LoginContext.getLoginInfo().get("id")取当前登录人。 - 微服务协作:mall-order 不直连购物车/商品的数据库,全部通过 OpenFeign(
CartClient、ProductClient)跨服务调用——这也是整个链路里最容易出现”服务没启动就报错”的地方。
一、阶段一:加入购物车
1. 前端入口(ProductDetail.vue)
//加入购物车
const addCart = () => {
if (soldOut.value) {
ElMessage.warning('该商品已售罄')
return
}
//只传商品id和数量——用户身份在token里,前端不传userId(防伪造)
cartApi.add({productId: product.value.id, count: count.value}).then(result => {
if (result.code === 1) {
ElMessage.success('已加入购物车')
cartCountStore.refreshCount() //刷新底部TabBar的角标
} else {
ElMessage.error(result.msg || '加入购物车失败')
}
})
}
// api/cart/cart.js
add(cart) {
return request.post("/cart", cart) // {productId, count}
}
要点:前端只传 productId + count,绝不传 userId——身份由后端从 token 解析,用户无法伪造”替别人加购物车”。
2. 后端落库(CartController.java + CartServiceImpl.java)
@PostMapping
public Result add(@RequestBody Cart cart) {
cartService.add(cart);
return Result.ok("添加成功");
}
@Override
public void add(Cart cart) {
Long id = (Long) LoginContext.getLoginInfo().get("id");
//归属当前登录用户(之前误写成setId,导致user_id为null)
cart.setUserId(id);
LambdaQueryWrapper<Cart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Cart::getUserId,id);
queryWrapper.eq(Cart::getProductId,cart.getProductId());
Cart dbcart = cartMapper.selectOne(queryWrapper);
if (dbcart != null) {
//同一商品已在购物车:数量累加,而不是插入重复行
dbcart.setCount((dbcart.getCount() + cart.getCount()));
cartMapper.updateById(dbcart);
}else {
cartMapper.insert(cart); //主键ASSIGN_ID雪花生成
}
}
要点:
- user_id 来自 ThreadLocal,即网关透传的 JWT 中的用户 id。
- “加购”是 upsert 语义:同一用户 + 同一商品只有一行,重复加购只累加数量。
- 曾踩的坑:
cart.setId(id)把登录用户 id 塞给了主键,插入时user_id为 null 报Field 'user_id' doesn't have a default value,且主键被污染成用户 id 会冲突。一字之差(setId/setUserId),这是本次联调真实踩过的 bug。
二、阶段二:购物车结算页
Cart.vue 列出当前用户购物车(GET /cart),每项可勾选(selected 0/1)、改数量、删除;底部结算栏显示合计,点击跳转 /order/confirm。
CartServiceImpl.listAll() 返回的每项带商品信息(批量 Feign 查商品回填,见《Redis缓存笔记》同款防 N+1 手法):
//一次性批量查回所有关联商品,按id建索引(避免循环内逐条Feign调用的N+1问题)
List<Long> productIds = cartList.stream().map(Cart::getProductId).filter(Objects::nonNull).distinct().toList();
Map<Long, Product> productMap = new HashMap<>();
if (!productIds.isEmpty()) {
Result<List<Product>> result = productClient.listByIds(productIds);
if (result.getCode() == Result.OK && result.getData() != null) {
productMap = result.getData().stream().collect(Collectors.toMap(Product::getId, p -> p));
}
}
前端 isValid(item) = !!item.product——商品已被删除时 product 为 null,显示”商品已失效”,不参与结算。
三、阶段三:生成订单
1. 前端确认订单(OrderConfirm.vue)
//提交订单:只传收货地址id,商品/价格/用户全由后端核定
const submitOrder = () => {
if (!currentAddress.value) {
ElMessage.warning('请先选择收货地址')
return
}
submitting.value = true
orderApi.create({shippingId: currentAddress.value.id}).then(result => {
submitting.value = false
if (result.code === 1) {
ElMessage.success('下单成功')
cartCountStore.refreshCount() //下单后购物车勾选项被清除,刷新角标
router.replace('/order') //跳到我的订单
} else {
ElMessage.error(result.msg || '下单失败')
}
})
}
要点:金额绝不由前端传——前端传的价格可以随意篡改,订单金额一律由后端按数据库里的商品价格重新计算。
2. 后端核心(OrderServiceImpl.add,整段是链路最重的一段)
@Override
public void add(Order order) {
//① 当前登录人(ThreadLocal ← 网关透传)
Long userId = (Long) LoginContext.getLoginInfo().get("id");
order.setUserId(userId);
//② Feign调cart-service,拿当前用户购物车里"已勾选"的商品(含商品信息)
Result<List<CartVO>> result = cartClient.list();
if (result.getCode() == Result.ERROR) {
throw new ServiceException("生成订单失败");
}
//保存订单基础字段
order.setStatus(OrderStatus.UNPAID.getCode()); //1:未支付
order.setPaymentType(1);
order.setPostage(0);
List<CartVO> cartVOList = result.getData();
List<CartVO> selectedCartVOList = cartVOList.stream()
.filter(cartVO -> cartVO.getSelected() == 1)
.toList();
//③ 用数据库里的商品价格计算总价(前端传的钱数一概不信)
BigDecimal payment = BigDecimal.ZERO;
for (CartVO cartVO : selectedCartVOList) {
payment = payment.add(cartVO.getProduct().getPrice()
.multiply(BigDecimal.valueOf(cartVO.getCount())));
// TODO 判断库存是否充足,如果库存不足,抛出异常
}
order.setPayment(payment);
//④ 插订单表(主键orderNo为雪花ID,插入后自动回填)
orderMapper.insert(order);
//⑤ 逐条插订单明细表:下单瞬间的商品名/图片/单价快照
for (CartVO cartVO : selectedCartVOList) {
OrderItem orderItem = new OrderItem();
orderItem.setOrderNo(order.getOrderNo());
orderItem.setUserId(userId);
orderItem.setProductId(cartVO.getProductId());
orderItem.setProductName(cartVO.getProduct().getName());
orderItem.setProductImage(cartVO.getProduct().getMainImage());
orderItem.setCurrentUnitPrice(cartVO.getProduct().getPrice());
orderItem.setQuantity(cartVO.getCount());
orderItem.setTotalPrice(cartVO.getProduct().getPrice()
.multiply(BigDecimal.valueOf(cartVO.getCount())));
orderItemMapper.insert(orderItem);
}
//⑥ 清掉已下单的购物车项(Feign逐条删除)
selectedCartVOList.forEach(cartVO -> {
cartClient.deletedById(cartVO.getId());
});
}
要点解读:
- 订单快照设计:
order_item存下单瞬间的productName / productImage / currentUnitPrice——之后商品改名、调价、甚至删除,历史订单展示不受影响。这是电商订单表的经典设计。 - 订单号是雪花 ID(
IdType.ASSIGN_ID):插入后从实体上拿回,后面支付宝支付的out_trade_no用的就是它。 - 跨服务事务问题:第⑥步清购物车如果失败,会出现”已下单但购物车还在”的脏数据——当前没有分布式事务(Seata),属于已知权衡;TODO 里还有库存校验、扣库存,同样是分布式一致性的练习点。
- Feign 调用链:
CartClient、ProductClient定义在mall-api公共模块,各服务共享;目标服务没启动时调用直接抛UnknownHostException(Nacos 里无实例)——联调时按服务名排查。
四、阶段四:支付宝沙箱支付
1. 配置(mall-order/application.yml + AlipayProperties)
# 支付宝沙箱配置(凭证建议通过环境变量注入:ALIPAY_APP_ID / ALIPAY_PRIVATE_KEY / ALIPAY_PUBLIC_KEY)
alipay:
appId: ${ALIPAY_APP_ID:9021000165698953}
privateKey: ${ALIPAY_PRIVATE_KEY:MIIEvA...} # 应用私钥(RSA2)
publicKey: ${ALIPAY_PUBLIC_KEY:MIIBIjAN...} # 支付宝公钥(验签用,注意不是应用公钥!)
gateway: ${ALIPAY_GATEWAY:https://openapi-sandbox.dl.alipaydev.com/gateway.do}
signType: RSA2
returnUrl: ${ALIPAY_RETURN_URL:http://localhost:5174/order} # 付款完浏览器跳回前台订单页
notifyUrl: ${ALIPAY_NOTIFY_URL:http://localhost:8088/order/notify} # 本地收不到外网回调(见联调限制)
@Data
@Configuration
@ConfigurationProperties(prefix = "alipay")
public class AlipayProperties {
private String appId;
private String privateKey;
private String publicKey;
private String gateway;
private String signType;
private String returnUrl;
private String notifyUrl;
}
密钥关系图(最容易搞混的点):
应用私钥(privateKey) ──签名──► 请求发给支付宝(证明"我是这个应用")
支付宝公钥(publicKey) ──验签──► 支付宝的异步通知/查单响应(证明"回调真的来自支付宝")
2. 支付宝适配层(AlipayServiceImpl)
@Slf4j
@Service
public class AlipayServiceImpl implements AlipayService {
@Autowired
private AlipayProperties alipayProperties;
//AlipayClient创建开销大且线程安全,懒加载单例复用
private volatile AlipayClient alipayClient;
private AlipayClient client() throws AlipayApiException {
if (alipayClient == null) {
synchronized (this) {
if (alipayClient == null) {
AlipayConfig cfg = new AlipayConfig();
cfg.setServerUrl(alipayProperties.getGateway()); //沙箱网关
cfg.setAppId(alipayProperties.getAppId());
cfg.setPrivateKey(alipayProperties.getPrivateKey());
cfg.setSignType(alipayProperties.getSignType());
cfg.setCharset("utf-8");
cfg.setFormat("json");
alipayClient = new DefaultAlipayClient(cfg);
}
}
}
return alipayClient;
}
@Override
public String createPayForm(Order order, List<OrderItem> itemList) {
if (itemList == null || itemList.isEmpty()) {
throw new RuntimeException("订单缺少商品明细,无法支付");
}
try {
//支付金额以订单表的payment为准(下单时已核算),金额格式必须是两位小数的字符串
BigDecimal totalAmount = order.getPayment().setScale(2, RoundingMode.HALF_UP);
//商品标题:取第一个商品名 + "等",超长截断
StringBuilder subject = new StringBuilder(itemList.get(0).getProductName());
if (itemList.size() > 1) {
subject.append("等").append(itemList.size()).append("件商品");
}
if (subject.length() > 90) {
subject.setLength(90);
}
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setNotifyUrl(alipayProperties.getNotifyUrl()); //异步回调:服务器到服务器
request.setReturnUrl(alipayProperties.getReturnUrl()); //同步回跳:支付完跳回前端页面
Map<String, Object> biz = new HashMap<>();
biz.put("out_trade_no", String.valueOf(order.getOrderNo())); //商户订单号:本项目为雪花orderNo
biz.put("total_amount", totalAmount.toPlainString());
biz.put("subject", subject.toString());
biz.put("product_code", "FAST_INSTANT_TRADE_PAY"); //电脑网站支付固定值
request.setBizContent(JSONUtil.toJsonStr(biz));
AlipayTradePagePayResponse response = client().pageExecute(request);
if (!response.isSuccess()) {
log.error("支付宝下单失败: code={}, msg={}, subMsg={}",
response.getCode(), response.getMsg(), response.getSubMsg());
throw new RuntimeException("支付宝下单失败:" + response.getSubMsg());
}
//返回的是一段自动提交的form表单HTML,前端插入页面后自动跳转沙箱收银台
return response.getBody();
} catch (AlipayApiException e) {
log.error("支付宝下单异常", e);
throw new RuntimeException("支付宝下单异常");
}
}
@Override
public String verifyNotify(Map<String, String> params) {
try {
//1.验签:必须用支付宝公钥验证,防止伪造回调
boolean signVerified = AlipaySignature.rsaCheckV1(params,
alipayProperties.getPublicKey(), "utf-8", alipayProperties.getSignType());
if (!signVerified) {
log.warn("支付宝异步通知验签失败, params={}", params);
return null;
}
//2.交易状态:TRADE_SUCCESS(支付成功)/TRADE_FINISHED(交易结束,不可退款)
String tradeStatus = params.get("trade_status");
if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) {
String outTradeNo = params.get("out_trade_no");
if (StringUtils.hasText(outTradeNo)) {
return outTradeNo; //支付成功,把订单号交给调用方流转状态
}
}
//验签通过但交易未成功(如买家未付款),回复success让支付宝停止重发
return "";
} catch (Exception e) {
log.error("处理支付宝异步通知异常", e);
return null;
}
}
@Override
public String queryTradeStatus(Long orderNo) {
try {
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
Map<String, Object> biz = new HashMap<>();
biz.put("out_trade_no", String.valueOf(orderNo));
request.setBizContent(JSONUtil.toJsonStr(biz));
AlipayTradeQueryResponse response = client().execute(request);
if (response.isSuccess()) {
return response.getTradeStatus();
}
//交易不存在:用户还没跳转或没付款(错误码ACQ.TRADE_NOT_EXIST),按待付款处理
log.info("支付宝查单: 订单{}无交易记录, subCode={}, subMsg={}",
orderNo, response.getSubCode(), response.getSubMsg());
return "WAIT_BUYER_PAY";
} catch (AlipayApiException e) {
log.error("支付宝查单异常, orderNo={}", orderNo, e);
throw new RuntimeException("查询支付状态异常");
}
}
}
设计要点:
- 职责边界:
AlipayServiceImpl是纯粹的”支付宝协议适配器”——验签、组装参数、调网关,不掺订单业务(不注入 IOrderService)。订单状态流转由 Controller 编排。这样依赖方向是单向的,不会出现OrderService ⇄ AlipayService循环依赖(曾因 handleNotify 内直接调 orderService 触发BeanCreationNotAllowedException循环依赖启动失败,重构为此方案)。 out_trade_no用雪花 orderNo:商户侧唯一订单号,支付宝凭它对账、查单、回调。product_code: FAST_INSTANT_TRADE_PAY:电脑网站支付(page pay)的固定值。- 金额是字符串且两位小数:
toPlainString()避免科学计数法,setScale(2, HALF_UP)满足支付宝对金额格式的要求。
3. 订单服务:校验 + 状态流转(OrderServiceImpl)
@Override
public String pay(Long orderNo, Long userId) {
Order order = orderMapper.selectById(orderNo);
if (order == null) {
throw new ServiceException("订单不存在");
}
//只能支付自己的订单
if (!userId.equals(order.getUserId())) {
throw new ServiceException("无权操作该订单");
}
//只有未支付订单可以发起支付
if (order.getStatus() == null || order.getStatus() != OrderStatus.UNPAID.getCode()) {
throw new ServiceException("订单当前状态不支持支付");
}
List<OrderItem> itemList = orderItemMapper.selectList(
new LambdaQueryWrapper<OrderItem>().eq(OrderItem::getOrderNo, orderNo));
return alipayService.createPayForm(order, itemList);
}
@Override
public void paySuccess(Long orderNo) {
Order order = orderMapper.selectById(orderNo);
//幂等保护:订单不存在、或已经不是"未支付"状态(重复通知),直接忽略
if (order == null || order.getStatus() == null || order.getStatus() != OrderStatus.UNPAID.getCode()) {
return;
}
Order update = new Order();
update.setOrderNo(orderNo);
update.setStatus(OrderStatus.PAID.getCode()); //1未支付 → 2已支付
update.setPaymentTime(new Date());
orderMapper.updateById(update);
}
@Override
public Integer syncPayStatus(Long orderNo, Long userId) {
Order order = orderMapper.selectById(orderNo);
if (order == null) {
throw new ServiceException("订单不存在");
}
//只能查询自己的订单
if (!userId.equals(order.getUserId())) {
throw new ServiceException("无权操作该订单");
}
//已经是终态的订单直接返回,不去打扰支付宝
if (order.getStatus() == null || order.getStatus() != OrderStatus.UNPAID.getCode()) {
return order.getStatus();
}
//未支付订单:向支付宝查询真实交易状态(异步通知收不到时的兜底)
String tradeStatus = alipayService.queryTradeStatus(orderNo);
if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) {
paySuccess(orderNo); //内部幂等,把状态流转为已支付
return OrderStatus.PAID.getCode();
}
//WAIT_BUYER_PAY(还没付)/TRADE_CLOSED(交易关闭):保持未支付
return OrderStatus.UNPAID.getCode();
}
状态机(OrderStatus 枚举):0已取消 → 1未支付 → 2已支付 → 3已发货 → 4交易成功 / 5已关闭。支付环节只负责 1 → 2 这一步,且两个入口(异步通知、主动查单)汇合到同一个 paySuccess,靠”只有未支付才更新”实现幂等——重复通知、通知与查单赛跑,都不会重复更新。
4. Controller 端点(OrderController)
//发起支付宝支付:返回自动提交的支付表单HTML
@GetMapping("/pay/{orderNo}")
public Result<String> pay(@PathVariable Long orderNo) {
Long userId = (Long) LoginContext.getLoginInfo().get("id");
//注意:必须用ok(msg,data)重载。pay返回String,直接ok(form)会命中ok(String msg)把表单塞进msg
return Result.ok("", orderService.pay(orderNo, userId));
}
//支付宝异步通知(服务器到服务器的POST,不带token)
@PostMapping(value = "/notify", produces = "text/plain;charset=UTF-8")
public String notify(@RequestParam Map<String, String> params) {
//验签并解析交易结果:null=验签失败;""=验签通过但交易未成功;有值=支付成功的订单号
String orderNo = alipayService.verifyNotify(params);
if (orderNo == null) {
return "failure";
}
if (!orderNo.isEmpty()) {
orderService.paySuccess(Long.parseLong(orderNo)); //内部幂等,重复通知不会重复更新
}
return "success";
}
//主动查单:向支付宝查询真实支付状态并同步订单(异步通知收不到时的兜底)
@GetMapping("/payStatus/{orderNo}")
public Result<Integer> payStatus(@PathVariable Long orderNo) {
Long userId = (Long) LoginContext.getLoginInfo().get("id");
return Result.ok(orderService.syncPayStatus(orderNo, userId));
}
//支付宝同步回跳(浏览器支付完成后跳回,仅承载跳转,支付结果以异步通知为准)
@GetMapping("/return")
public Map<String, String> returnUrl(@RequestParam Map<String, String> params) {
return params;
}
三个必须知道的协议细节:
/notify的返回值必须是纯文本success——支付宝收到 success 才停止重发通知,否则按衰减间隔最多重发 8 次。所以不能包ResultJSON。/notify不带任何 token——必须在网关AuthGlobalFilter白名单精确放行path.startsWith("/order/notify"),同时接口内必须验签(这是防伪造”支付成功”的唯一防线,验签不过直接failure)。/notify用@RequestParam收参——支付宝回调是application/x-www-form-urlencoded表单,Spring 自动把每个表单字段收进 Map,供rsaCheckV1原样验签(多参少参都会导致验签失败)。
5. 网关白名单(AuthGlobalFilter.java)
//不需要进行验证的路径
if (path.startsWith("/service/captcha")
|| path.startsWith("/admin/login")
|| path.startsWith("/user/login")
|| path.startsWith("/order/notify")) { //支付宝异步通知是服务器回调,不带token
return chain.filter(exchange);
}
五、阶段五:前端支付交互(OrderList.vue)
//去支付:后端返回支付宝的form表单HTML,插入页面后自动提交跳转沙箱收银台
const pay = (order) => {
orderApi.pay(order.orderNo).then(result => {
if (result.code === 1) {
//记下这笔订单,付款跳回来后自动查单核对状态
localStorage.setItem('payingOrderNo', String(order.orderNo))
const div = document.createElement('div')
div.style.display = 'none'
div.innerHTML = result.data //后端返回的表单HTML
document.body.appendChild(div)
const form = div.querySelector('form')
if (form) {
form.submit() //自动提交 → 浏览器带着签名参数跳到支付宝收银台
} else {
ElMessage.error('支付表单异常')
}
} else {
ElMessage.error(result.msg || '发起支付失败')
}
})
}
//主动查单兜底:本地收不到支付宝的异步通知,跳回来后向支付宝核对一次真实支付状态
const checkPayingOrder = () => {
const payingOrderNo = localStorage.getItem('payingOrderNo')
if (!payingOrderNo) return
localStorage.removeItem('payingOrderNo')
const order = list.value.find(o => String(o.orderNo) === payingOrderNo)
//订单不在列表里或已不是待付款,就不用查了
if (!order || order.status !== 1) return
orderApi.payStatus(payingOrderNo).then(result => {
if (result.code === 1) {
order.status = result.data
if (result.data === 2) {
ElMessage.success('支付成功')
} else {
ElMessage.info('订单尚未完成支付')
}
}
}).catch(() => {})
}
模板上:order.status === 1(待付款)的订单显示”去支付”按钮。
交互设计:为什么用 localStorage 记订单号?付款是在支付宝页面完成的,回来时是整页重新加载,内存状态全丢,只能靠 localStorage 这种跨页面刷新持久的存储把”我正在付哪单”带回来;查完立即 remove,防止下次进页面误查。
六、本地联调的已知限制与踩坑记录
1. 异步通知到不了本机(限制,非 bug)
notifyUrl 若是 localhost/192.168.x.x,支付宝服务器无法发起回调——localhost 只在本机有效,私网地址不在公网路由。本地跑闭环靠主动查单兜底(阶段五);生产部署到公网服务器后,异步通知成为主力(实时、不依赖用户回跳),查单退居兜底。参考项目 order_service 之所以通知正常,是因为它整套部署在公网 mall.dxkua.cn。备选方案是内网穿透(注意只穿透必要端口、用完即关)。
2. 实际踩过的四个坑
| 坑 | 现象 | 原因与修复 |
|---|---|---|
Result.ok(String) 重载陷阱 |
支付表单 HTML 被塞进 msg,data 为 null,前端报”支付表单异常” |
pay() 返回 String,命中 ok(String msg) 而非 ok(Object data);改用 ok("", form) 显式走双参重载 |
| 循环依赖启动失败 | OrderServiceImpl ⇄ AlipayServiceImpl 互相注入,BeanCreationNotAllowedException |
重构职责:AlipayService 只做支付宝协议,返回解析结果,订单状态流转由 Controller 编排,依赖变单向 |
Field 'user_id' doesn't have a default value |
加购物车插入失败 | cart.setId(id) 误把用户 id 塞给主键,应为 cart.setUserId(id) |
| 收银台控制台一堆红字 | ERR_NAME_NOT_RESOLVED、Mixed Content、wss://127.0.0.1 连接失败 |
全是支付宝沙箱页面自己的埋点/指纹采集/安全控件探测噪音,不影响支付功能,可无视 |
3. 验证清单
- 详情页加购 → TabBar 角标 +1;重复加购数量累加
- 购物车勾选 → 去结算 → 确认订单页金额 = 数据库价格 × 数量
- 提交订单 → 我的订单出现”待付款”,对应购物车项被清除
- 去支付 → 跳沙箱收银台(控制台红字无视)→ 沙箱买家账号付款
- 跳回订单页 → 自动弹”支付成功” → 订单状态变”已付款”、数据库
payment_time有值