背景:
随着用户量不断增加,服务器成本越来越大。想着实现会员制回点服务器成本。
业务场景分析:
用户在站点上付款 -----> 我监听到付款金额 -----> 给用户开通会员
调研:
-
支付宝和微信官方支付接口:基本都需要企业资格才能开通,最起码也要是个体工商户才可以(有营业执照)
-
第三方支付平台:例如图灵支付,xpay等,支持个人开发者,但是手续费太高。
-
野路子:网上有开源方案是监听支付宝app收款通知,实现收款,例如PaysApi、绿点支付等,本质上依然是采用挂机监听的策略,但针对的是移动端支付宝或微信的收款通知消息,成本高,配置麻烦,需24小时挂台安卓手机,不免费。
-
使用第三方卡密平台进行发卡。因为手续费、提现规则等各种原因放弃。
调研结果:
支付宝当面付:支持个人开通,但是需要门店照片,这个百度就可以
营业执照是可选的,不上传的话,限制单笔收款≤1000,单日收款≤5W,对于个人开发者足够了。
效果图:
二维码支付成功后,执行自己的业务逻辑。例如给用户开通会员。
昵称金色展示。
接入流程:
1.点击这里进入,登陆支付宝账户选择立即接入。
2.经营内容选择百货零售-超市-超市(非平台类)
3.营业执照可不上传
4.店铺招牌 百度即可
5.提交申请后十多分钟就可收到通过通知。
可参考这个同学的文章:【应用申请开通和配置】
开发流程:
成功接入以后,可以在蚂蚁金服开放平台 网页&移动应用中,看到我的应用列表中多了一个“应用2.0签约******”的应用: 或者是你自己起名字的应用
现在我们可以开发接入了,总体分为以下几个步骤(参考当面付文档 ,当面付开发流程):
-
配置当面付公钥私钥
找到 你的 应用,点击右侧查看详情
-
在应用信息中设置公钥
支付宝官方提供了密钥生成工具,很简单,使用工具生成应用公钥和私钥,应用公钥设置到支付宝,应用私钥保存到本地,应用公钥设置到支付宝后,支付宝会生成一个支付宝公钥,保存到本地。具体参见这里 -
回调地址配置
总结:
借鉴三个同学的文章 + 并结合GPT4调试代码 + 返回base64码方便前端展示。 改造优化:
【自己个人拥有一个可以支付功能的网站?当然可以了!保姆级演示!】
【个人支付方案(免签约)-支付宝当面付】
【zxing生成二维码】
结合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
@Controller
public 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>
八爪鱼现金流