背景: 随着用户量不断增加,服务器成本越来越大。想着实现会员制回点服务器成本。
业务场景分析:
用户在站点上付款 -----> 我监听到付款金额 -----> 给用户开通会员
调研:
-
支付宝和微信官方支付接口:基本都需要企业资格才能开通,最起码也要是个体工商户才可以(有营业执照)
-
第三方支付平台:例如图灵支付,xpay等,支持个人开发者,但是手续费太高。
-
野路子:网上有开源方案是监听支付宝app收款通知,实现收款,例如PaysApi、绿点支付等,本质上依然是采用挂机监听的策略,但针对的是移动端支付宝或微信的收款通知消息,成本高,配置麻烦,需24小时挂台安卓手机,不免费。
-
使用第三方卡密平台进行发卡。因为手续费、提现规则等各种原因放弃。
调研结果:
[支付宝当面付] 地址:
https://b.alipay.com/page/product-workspace/all-product?productId=I1011000290000001003
支持个人开通,但是需要门店照片,这个百度就可以
营业执照是可选的,不上传的话,限制单笔收款≤1000,单日收款≤5W,对于个人开发者足够了。
效果图:
二维码支付成功后,执行自己的业务逻辑。例如给用户开通会员。
昵称金色展示。
接入流程:
1.[点击这里进入]
(https://b.alipay.com/page/product-workspace/all-product?productId=I1011000290000001003),登陆支付宝账户选择立即接入。
2.经营内容选择百货零售-超市-超市(非平台类)
3.营业执照可不上传
4.店铺招牌 百度即可
5.提交申请后十多分钟就可收到通过通知。
可参考这个同学的文章:应用申请开通和配置
https://blog.csdn.net/qq_40881680/article/details/128406119
开发流程:
成功接入以后,可以在
[蚂蚁金服开放平台]
https://openhome.alipay.com/platform/appManage.htm#/apps
网页&移动应用中,看到我的应用列表中多了一个"应用2.0签约******"的应用: 或者是你自己起名字的应用
现在我们可以开发接入了,总体分为以下几个步骤
参考[当面付文档]
https://docs.open.alipay.com/194/
当面付[开发流程]
https://fw.alipay.com/alipaymarket/ability/SM010000000000001000/detail.htm#anchor-accessSchedule
- 配置当面付公钥私钥
找到 你的 应用,点击右侧查看详情
- 在应用信息中设置公钥
支付宝官方提供了密钥生成工具,很简单,使用工具生成应用公钥和私钥,应用公钥设置到支付宝,应用私钥保存到本地,应用公钥设置到支付宝后,支付宝会生成一个支付宝公钥,保存到本地。
[具体参见这里]
https://docs.open.alipay.com/291/105971
- 回调地址配置
总结:
借鉴三个同学的文章 + 并结合GPT4调试代码 + 返回base64码方便前端展示。 改造优化:
【自己个人拥有一个可以支付功能的网站?当然可以了!保姆级演示!】
https://blog.csdn.net/qq_40881680/article/details/128406119
【个人支付方案(免签约)-支付宝当面付】
https://blog.csdn.net/rankun1/article/details/92401295
【zxing生成二维码】
https://blog.csdn.net/gdgztt/article/details/134756196
结合GPT4,代码报错改造优化
java.lang.UnsatisfiedLinkError: /usr/local/java/jdk1.8.0_152/jre/lib/amd64/libawt_xawt.so: libXrender.so.1: cannot open shared object file: No such file or directory
oro.sprinofrananork.neb.util.mestedserletEexception: Hanmer dispatch faled; nested exception is famna.amt.Eropr: can't comet to xll window sener usin0 "locahost:10.0 as the vawe of the DIspl variabl
简单示例代码:
示例主要流程代码。后续优化,可自行调整,比如,金额配置到数据库或者配置中心,金额校验。 为简化,只贴出主要代码。请自行继续优化。
1.Maven引入需要的jar包 * * * * * * * * * * * *
<!--alipay SDK--><dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk-java</artifactId> <version>4.35.9.ALL</version></dependency><!-- zxing --><dependency> <groupId>com.google.zxing</groupId> <artifactId>core</artifactId> <version>3.5.1</version></dependency>
2.Controller代码
package com.kaihang.my.money.web.admin.web.controller;
import com.alipay.api.AlipayApiException;import com.alipay.api.internal.util.AlipaySignature;import com.kaihang.my.money.dao.entity.TbUser;import com.kaihang.my.money.dao.entity.TbUserOrder;import com.kaihang.my.money.dao.entity.TbUserOrderExample;import com.kaihang.my.money.dao.mapper.TbUserOrderMapper;import com.kaihang.my.money.web.admin.util.AliPayUtil;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.util.CollectionUtils;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;import java.util.Base64;import java.util.Date;import java.util.HashMap;import java.util.Iterator;import java.util.List;import java.util.Map;
/** * 支付回调接口 */@Slf4j@Controllerpublic class AliPayController {
@Autowired private TbUserOrderMapper tbUserOrderMapper;
@RequestMapping(value = "/alipay/queryCode",method = RequestMethod.POST) @ResponseBody public HashMap<String,String> queryCode(HttpServletRequest request,TbUser tbUser){ HashMap<String,String> resultMap = new HashMap<>();
String onemonthVal = createQcode("八爪鱼1个月会员","onemonth","5",tbUser); resultMap.put("onemonth",onemonthVal);
String threemonthVal = createQcode("八爪鱼3个月会员","threemonth","15",tbUser); resultMap.put("threemonth",threemonthVal); // 55元 String oneyearVal = createQcode("八爪鱼1年会员","oneyear","55",tbUser); resultMap.put("oneyear",oneyearVal);
return resultMap;
}
public String createQcode(String productName,String productPrefix,String totalPrice,TbUser tbUser){
//自己生成一个订单号,我这里直接用时间戳演示,正常情况下创建完订单需要存储到自己的业务数据库,做记录和支付完成后校验 // 前缀pay + 1个月的会员onemonth +userId + 加时间戳 String orderNo = "pay"+productPrefix+tbUser.getId() + System.currentTimeMillis();
Date now = new Date(); // 保存到自己设计的订单表 order ,可自己设计。 saveUserOrder(productName,orderNo,totalPrice,tbUser,now);
// 获取到静态资源的绝对路径 // String logoPath = servletContext.getRealPath("/static/assets/img/logo1.jpg"); String logoPath = ""; //传递空就行,没必要加logo
byte[] qRcode = AliPayUtil.getQRcode(productName, orderNo, totalPrice, logoPath); return Base64.getEncoder().encodeToString(qRcode); }
public boolean saveUserOrder(String productName,String orderNo,String totalPrice,TbUser tbUser,Date now){ TbUserOrder userOrder = new TbUserOrder(); userOrder.setUserId(tbUser.getId()+""); userOrder.setUserEmail(tbUser.getEmail()); userOrder.setTotalPrice(totalPrice); userOrder.setOrderNo(orderNo); userOrder.setProductName(productName); userOrder.setBuyTime(now); userOrder.setCreateTime(now); userOrder.setOrderStatus("新建未支付"); userOrder.setValidInd("1"); int insert = tbUserOrderMapper.insert(userOrder); // 如果大于0,保存成功,返回true return insert>0; }
/** * 支付成功回调接口 * @return */ @RequestMapping(value = "/alipay/bazhuayu/callback",method = RequestMethod.POST) public Object callback(HttpServletRequest request){ log.info("【===支付宝回调开始===】");
Map<String, String> params = new HashMap<>();
Map requestParams = request.getParameterMap(); for(Iterator iter = requestParams.keySet().iterator(); iter.hasNext();){ String name = (String)iter.next(); String[] values = (String[]) requestParams.get(name); String valueStr = ""; for(int i=0; i<values.length;i++){ valueStr = (i == values.length-1) ? valueStr + values[i] : valueStr + values[i] + ","; } params.put(name,valueStr); } log.info("支付宝回调: sign:{}, trade_status:{}, 参数:{}",params.get("sign"),params.get("trade_status"),params.toString());
//验证回调的正确性:是不是支付宝发的 String alipayPublicKey = "xxxxxxxxxxxx"; // 你的支付宝公钥。TODO 替换为你自己的支付宝公钥!!!! String signType = "RSA2"; params.remove("sign_type"); try { //这里使用的是支付宝提供的验签方式 boolean alipayRSACheckedV2 = AlipaySignature.rsaCheckV2(params, alipayPublicKey,"utf-8",signType); if(!alipayRSACheckedV2) { // return ServerResponse.createByErrorMessage("非法请求,验证不通过!"); // throw new RuntimeException("非法请求,验证不通过!"); log.info("非法请求,验证不通过!"); return "failed"; } } catch (AlipayApiException e) { log.error("支付宝回调异常",e); }
//订单支付后修改订单状态 // 订单金额,订单号 out_trade_no=payoneyear81714208576353 订单状态修改。同时给开通对应的会员天数
String outTradeNo = params.get("out_trade_no"); String totalAmount = params.get("total_amount");
TbUserOrderExample example = new TbUserOrderExample(); example.createCriteria().andValidIndEqualTo("1").andOrderNoEqualTo(outTradeNo); List<TbUserOrder> tbUserOrders = tbUserOrderMapper.selectByExample(example);
if(!CollectionUtils.isEmpty(tbUserOrders)){ TbUserOrder tbUserOrder = tbUserOrders.get(0); if(null != tbUserOrder){ String totalPrice = tbUserOrder.getTotalPrice(); // 校验金额 if(totalPrice.equals(totalAmount)){ // 旧的支付状态 String oldOrderStatus = tbUserOrder.getOrderStatus();
tbUserOrder.setOrderStatus("支付成功"); tbUserOrderMapper.updateByPrimaryKey(tbUserOrder);
// 成功之后,会员的,添加会员时间 TbUser tbUser = addHuiYuan(tbUserOrder,oldOrderStatus);
//返回支付状态给支付宝,避免支付宝重复通知 return "TRADE_SUCCESS"; }
} }
return "failed"; }
public TbUser addHuiYuan(TbUserOrder tbUserOrder,String oldOrderStatus){ // 增加会员天数 // TODO 这里写你自己的业务处理逻辑。回调之后执行会到这里。 return null;
}
}
3.AliPayUtil工具类 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
package com.kaihang.my.money.web.admin.util;
/** * @Description: 支付宝-面对面支付 * * @Author: * @Date: 2024-04-23 16:09:33 */import com.alipay.easysdk.factory.Factory;import com.alipay.easysdk.kernel.BaseClient;import com.alipay.easysdk.payment.facetoface.models.AlipayTradePrecreateResponse;import org.slf4j.Logger;import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO;import java.awt.image.BufferedImage;import java.io.ByteArrayOutputStream;
public class AliPayUtil {
private static Logger logger = LoggerFactory.getLogger(AliPayUtil.class);
public static byte[] getQRcode(String subject, String orderNo, String totalAmount,String logoPath) { // 1. 设置参数(全局只需设置一次) Factory.setOptions(getOptions()); try { // 2. 发起API调用(使用面对面支付中的预下单) AlipayTradePrecreateResponse response = Factory.Payment.FaceToFace(). preCreate(subject,orderNo, totalAmount); // 3. 处理响应或异常 if ("10000".equals(response.code)) { logger.info("调用成功:{}",response.qrCode);
//获取生成的二维码,这里是一个String字符串,即二维码的内容; //然后用二维码生成SDK生成一下二维码,弄成图片返回给前端就行,我这里使用Zxing生成 //其实也可以直接把这个字符串信息返回,让前端去生成,一样的道理,只需要关心这个二维码的内容就行 String qrCode = response.qrCode;
//生成支付二维码图片 BufferedImage image = QRCodeUtil.createImage(qrCode,logoPath,true);
ByteArrayOutputStream out = new ByteArrayOutputStream(); ImageIO.write(image, "jpeg", out); byte[] b = out.toByteArray(); out.write(b); out.close(); //最终返回图片 return b; } else { logger.error("调用失败,原因:{},{}",response.msg,response.subMsg); } } catch (Exception e) { logger.error("调用遭遇异常,原因:{}",e.getMessage()); throw new RuntimeException(e.getMessage(), e); } return null; }
private static BaseClient.Config getOptions() { BaseClient.Config config = new BaseClient.Config(); config.protocol = "https"; config.gatewayHost = "openapi.alipay.com"; config.signType = "RSA2";
// 请更换为您的AppId config.appId = "xxxxxxxxxx"; // 请替换为您的AppId
// 请更换为您的PKCS8格式的应用私钥 config.merchantPrivateKey = "应用私钥RSA2048-敏感数据,请妥善保管xxxxxxxxxx"; // TODO 替换为你的支付宝私钥
// 支付宝公钥 config.alipayPublicKey = "xxxxxxxx"; // TODO 替换为你的支付宝公钥
config.notifyUrl = "https://你的域名/alipay/bazhuayu/callback";//这里是支付宝接口回调地址 成功后会调用AliPayController的callback方法
return config; }}
4.QRCodeUtil工具类 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
package com.kaihang.my.money.web.admin.util;
import com.google.zxing.BinaryBitmap;import com.google.zxing.DecodeHintType;import com.google.zxing.MultiFormatReader;import com.google.zxing.Result;import com.google.zxing.client.j2se.BufferedImageLuminanceSource;import com.google.zxing.common.HybridBinarizer;
import javax.imageio.ImageIO;import java.awt.image.BufferedImage;import java.io.File;import java.io.OutputStream;import java.util.Hashtable;import java.util.Random;
/** * @Description: 生成二维码 * @Author: 29489 * @Date: 2024-04-23 17:41:42 */public class QRCodeUtil { private static final String CHARSET = "utf-8"; private static final String FORMAT = "JPG"; // 二维码尺寸 private static final int QRCODE_SIZE = 300; // LOGO宽度 private static final int LOGO_WIDTH = 60; // LOGO高度 private static final int LOGO_HEIGHT = 60;
public static BufferedImage createImage(String content, String logoPath, boolean needCompress) throws Exception { BufferedImage result = QRCodeGenerator.generateQRCodeImage(content); return result; }
/** * 生成二维码(内嵌LOGO) * 二维码文件名随机,文件名可能会有重复 * * @param content * 内容 * @param logoPath * LOGO地址 * @param destPath * 存放目录 * @param needCompress * 是否压缩LOGO * @throws Exception */ public static String encode(String content, String logoPath, String destPath, boolean needCompress) throws Exception { BufferedImage image = QRCodeUtil.createImage(content, logoPath, needCompress); mkdirs(destPath); String fileName = new Random().nextInt(99999999) + "." + FORMAT.toLowerCase(); ImageIO.write(image, FORMAT, new File(destPath + "/" + fileName)); return fileName; }
/** * 生成二维码(内嵌LOGO) * 调用者指定二维码文件名 * * @param content * 内容 * @param logoPath * LOGO地址 * @param destPath * 存放目录 * @param fileName * 二维码文件名 * @param needCompress * 是否压缩LOGO * @throws Exception */ public static String encode(String content, String logoPath, String destPath, String fileName, boolean needCompress) throws Exception { BufferedImage image = QRCodeUtil.createImage(content, logoPath, needCompress); mkdirs(destPath); fileName = fileName.substring(0, fileName.indexOf(".")>0?fileName.indexOf("."):fileName.length()) + "." + FORMAT.toLowerCase(); ImageIO.write(image, FORMAT, new File(destPath + "/" + fileName)); return fileName; }
/** * 当文件夹不存在时,mkdirs会自动创建多层目录,区别于mkdir. * (mkdir如果父目录不存在则会抛出异常) * @param destPath * 存放目录 */ public static void mkdirs(String destPath) { File file = new File(destPath); if (!file.exists() && !file.isDirectory()) { file.mkdirs(); } }
/** * 生成二维码(内嵌LOGO) * * @param content * 内容 * @param logoPath * LOGO地址 * @param destPath * 存储地址 * @throws Exception */ public static String encode(String content, String logoPath, String destPath) throws Exception { return QRCodeUtil.encode(content, logoPath, destPath, false); }
/** * 生成二维码 * * @param content * 内容 * @param destPath * 存储地址 * @param needCompress * 是否压缩LOGO * @throws Exception */ public static String encode(String content, String destPath, boolean needCompress) throws Exception { return QRCodeUtil.encode(content, null, destPath, needCompress); }
/** * 生成二维码 * * @param content * 内容 * @param destPath * 存储地址 * @throws Exception */ public static String encode(String content, String destPath) throws Exception { return QRCodeUtil.encode(content, null, destPath, false); }
/** * 生成二维码(内嵌LOGO) * * @param content * 内容 * @param logoPath * LOGO地址 * @param output * 输出流 * @param needCompress * 是否压缩LOGO * @throws Exception */ public static void encode(String content, String logoPath, OutputStream output, boolean needCompress) throws Exception { BufferedImage image = QRCodeUtil.createImage(content, logoPath, needCompress); ImageIO.write(image, FORMAT, output); }
/** * 生成二维码 * * @param content * 内容 * @param output * 输出流 * @throws Exception */ public static void encode(String content, OutputStream output) throws Exception { QRCodeUtil.encode(content, null, output, false); }
/** * 解析二维码 * * @param file * 二维码图片 * @return * @throws Exception */ public static String decode(File file) throws Exception { BufferedImage image; image = ImageIO.read(file); if (image == null) { return null; } BufferedImageLuminanceSource source = new BufferedImageLuminanceSource(image); BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); Result result; Hashtable<DecodeHintType, Object> hints = new Hashtable<DecodeHintType, Object>(); hints.put(DecodeHintType.CHARACTER_SET, CHARSET); result = new MultiFormatReader().decode(bitmap, hints); String resultStr = result.getText(); return resultStr; }
/** * 解析二维码 * * @param path * 二维码图片地址 * @return * @throws Exception */ public static String decode(String path) throws Exception { return QRCodeUtil.decode(new File(path)); }
}
5.前端代码 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
<%@ page contentType="text/html;charset=UTF-8" language="java" %><!DOCTYPE html><html><head> <title>八爪鱼官网-会员购买页面</title> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <!-- SEO 关键字 --> <meta name="keywords" content="八爪鱼,财务自由,财务,财务自由之路,什么叫财务自由,财务自由需要多少资产,什么叫被动收入,打造被动收入,增加被动收入,怎样获得被动收入,价值投资,个人资产管理,理财,躺着赚钱,让钱为我打工,市场风云"> <meta name="description" content="个人资产管理平台,帮助您打造被动收入,发现投资机会,助力实现财务自由。财务自由、被动收入、个人资产管理、价值投资平台"> <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
<style> * { box-sizing: border-box; } body { font-family: 'Arial', sans-serif; background-color: #1a1a1a; /* 页面的背景色 */ color: #ccc; /* 文字颜色 */ margin: 0; padding: 0; display: flex; flex-direction: column; /* 使导航栏在顶部 */ } .navbar { position: relative; /* 如果之前没有设置,现在需要设置为相对定位 */ background-color: #202020; /* 导航栏背景色 */ padding: 10px 20px; display: flex; justify-content: space-between; /* 企业名称和导航项分开 */ align-items: center; } .navbar .logo { color: #fff; font-weight: bold; /* 企业名称字体加粗 */ font-size: 24px; }
/* 确保主内容区域有足够的下边距,以免被固定位置的footer遮挡 */ .main-content { margin-top: 50px; /* 导航栏的高度 */ padding-bottom: 40px; /* 根据新的footer高度调整,确保内容可见 */ }
/* 其他样式保持不变 */ .main-content { display: flex; justify-content: center; align-items: center; flex-direction: column; height: 100vh; /* 确保 .main-content 高度充满视口,以便居中 */ text-align: center; /* 文本居中 */ color: #FFFFFF; /* 设置文字颜色为亮白色 */ }
/* 保持原有的logo样式,现在应用于a标签 */ .logo { color: #fff; font-weight: bold; /* 企业名称字体加粗 */ font-size: 24px; text-decoration: none; /* 去除链接下划线 */ display: inline-block; /* 或其他适合的显示方式,确保布局正确 */ }
/* 可选:指定鼠标悬停在logo上时的样式,例如改变颜色 */ .logo:hover { color: #e0e0e0; /* 鼠标悬停时的颜色,可自定义 */ }
.qr-codes-container { display: flex; justify-content: center; /* 子元素水平居中 */ flex-wrap: wrap; /* 允许子元素在容器满时换行 */ gap: 20px; /* 子元素之间的间隔 */ width: 100%; /* 充满父容器宽度 */ max-width: 1200px; /* 最大宽度,根据需要调整 */ margin: 137px 20px auto; /* 上下保持20px,左右auto使得容器居中 */ }
.qr-code { text-align: center; /* Add additional styling as needed */ }
/* 二维码图片的样式,根据需要增加尺寸 */ .qr-code img { width: 250px; /* 图片宽度,根据需要调整 */ height: auto; /* 高度自动,保持图片比例 */ }
/* 二维码描述的样式 */ .qr-code p { color: #ffffff; /* 保持文字颜色为白色 */ font-size: 1rem; /* 调整字体大小为1rem,根据需要调整 */ }</style>
</head><body>
<div class="navbar"> <a href="/moneyTotal" class="logo">八爪鱼</a></div>
<div class="main-content"> <p>注:此页面暂时不自动跳转,购买支付成功后,请您重新登录网站!!!</p> <p style="font-size: 14px">如有其他问题请邮件联系我们:294894616@qq.com</p> <div class="qr-codes-container"> <div class="qr-code"> <img src="data:image/png;base64,${onemonth}" alt="八爪鱼1个月会员(5元)"/> <p>1个月会员(5元)</p> </div> <div class="qr-code"> <img src="data:image/png;base64,${threemonth}" alt="八爪鱼3个月会员(15元)"/> <p>3个月会员(15元)</p> </div> <div class="qr-code"> <img src="data:image/png;base64,${oneyear}" alt="八爪鱼1年会员(55元)"/> <p>1年会员(55元)</p> </div>
<p style="font-size: 14px; color: #f39c12">会员权益:尊享金色会员标识。月报、资产包、负债包、机会卡额度限制放开。</p>
</div>
</div>
</body></html>
网站示例:
八爪鱼现金流 https://www.incom.top
代码详情查看 csdn博客地址:
https://blog.csdn.net/u011055858/article/details/139558558