主要是从.properties
等文件中加载资源文件。使用静态资源代码块是为了让此类文件加载一次,防止多次访问多次加载耗费资源。
public class PropertiesUtil {
//这里是要获取参数
private static Logger logger = LoggerFactory.getLogger(PropertiesUtil.class);
//一个属性props
private static Properties props;
//属性文件的加载
//初始化props属性文件:因为需要只加载一次,所以使用静态代码块
static {
String fileName = "mmall.properties";
props = new Properties();
//加载资源文件 如何加载为与mmall.properties处的文件
try {
props.load(new InputStreamReader(PropertiesUtil.class.getClassLoader().getResourceAsStream(fileName),"UTF-8"));
} catch (IOException e) {
//打印日志.输出异常原因
logger.error("资源文件加载异常",e);
}
}
/*
两个trim()
一个是获取到的键值有空格
一个是获取到的值有空格
*/
//获取资源文件
public static String getProperty(String key){
//获取资源文件的值
String value = props.getProperty(key.trim());
if(StringUtils.isBlank(value)){
return StringUtils.EMPTY;
}
return value.trim();
}
//这个是含有默认值的返回资源文件的获取
public static String getProperty(String key, String defaultValue){
String value = props.getProperty(key.trim());
if(StringUtils.isBlank(value)){
value = defaultValue;
}
return value.trim();
}
}
使用joda.time
做时间转换还是比较简单的,例子:
public class testUtil {
//使用joda-time来对时间做格式转换
//str -> Date
public static Date strToDate(String dateTimeStr, String dateFormatStr){
DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern(dateFormatStr);
DateTime dateTime = dateTimeFormatter.parseDateTime(dateTimeStr);
return dateTime.toDate();
}
//Date -> str
public static String dateToStr(Date date, String dateFormatStr){
//首先判断时间是否为空
if(date == null){
return "";
}
DateTime dateTime = new DateTime(date);
//注意日期格式需要放进来
return dateTime.toString(dateFormatStr);
}
public static void main(String[] args) {
System.out.println(testUtil.strToDate("2019-05-07 17:06:29","yyyy-MM-dd HH:mm:ss"));
System.out.println(testUtil.dateToStr(new Date(),"yyyy-MM-dd HH:mm:ss"));
}
}
VO:value Object,使用看来是可以对一个对象进行封装,可以给一个对象加入之前它所没有的一些内容,比方说你有一本故事书,里面有很多个小的故事,但是如果有一天你觉得你的故事书内容太少,想给它加一些关于你感感兴趣的内容的时候,你就可以使用OV,创建一个BookStoryVo对象,加上一些你所要的内容,这个时候我们可以在原有的BookStroy上加上一些新的内容然后返回给自己。
一般这种扩展是用在我们自定义的返回类中(容纳的数据都是泛型形式的)
//BookStory类(省略了set/get)
public class BookStroy {
private String story1;
private String story2;
private String story3;
}
//BookStoryVo类(扩展了你所要的内容)
public class BookStoryVo {
private String story1;
private String story2;
private String story3;
private String people1;
private String people2;
}
//测试方法
public class Test {
public static void main(String[] args) {
BookStroy bookStroy = new BookStroy("story1","story2","story3");
BookStoryOv bookStoryOv = assemblyBookStory(bookStroy);
System.out.println(bookStoryOv);
}
//获取
public static BookStoryOv assemblyBookStory(BookStroy stroy){
BookStoryOv bookStoryOv = new BookStoryOv();
bookStoryOv.setStory1(stroy.getStory1());
bookStoryOv.setStory2(stroy.getStory2());
bookStoryOv.setStory3(stroy.getStory3());
bookStoryOv.setPeople1("people1");
bookStoryOv.setPeople2("people2");
return bookStoryOv;
}
}
查询相同层级的节点传入一个父节点,在数据库中查询以此为父节点的所有的子节点,即兄弟节点。查询所有的子节点需要使用到递归的逻辑。通过产品的品类ID
获取到产品,遍历所有的产品,将其放入创建的Set
集合中,由于递归需要一个基线条件和递归条件,基线条件就是递归终止条件,终止条件用当前品类的所有的兄弟节点遍历完全之后终止,然后返回Set
集合。
public ServerResponse<List<Category>> selectChildrenParallelCategory(Integer parentId){
//否则查询当前
List<Category> categoryList = categoryMapper.selectChildrenParallelCategory(parentId);
if(CollectionUtils.isEmpty(categoryList)){
return ServerResponse.createByErrorMessage("未找到当前分类的子分类");
}
return ServerResponse.createBySuccess(categoryList);
}
public ServerResponse<List<Integer>> selectCategoryAndChildrenCategoryById(Integer categoryId){
Set<Category> categorySet = Sets.newHashSet();
findChildrenCategory(categorySet, categoryId);
//两个都是Guaua的数据结构
List<Integer> categoryList = Lists.newArrayList();
if(categoryId != null){
for(Category categoryItem : categorySet){
categoryList.add(categoryItem.getId());
}
}
return ServerResponse.createBySuccess(categoryList);
}
private Set<Category> findChildrenCategory(Set<Category> categorySet,int categoryId){
//通过categoryId查找所有的对象,然后通过递归categoryId获取到所有的子对象
Category category = categoryMapper.selectByPrimaryKey(categoryId);
if (category != null) {
categorySet.add(category);
}
//遍历获得id 这里就是终止条件
List<Category> categoryList = categoryMapper.selectChildrenParallelCategory(categoryId);
//因为如果这个为空也不会报错,所以使用遍历的方法,一旦为空就退出循环
for(Category categoryItem : categoryList){
findChildrenCategory(categorySet, categoryItem.getId());
}
return categorySet;
}
在SpringMVC
要使用文件上传,需要使用到MultipartFile
这个类的文件,具体的步骤,先获取到传入文件的原始路径,再通过分割将文件的扩展名获取到,为了防止多个用户上传相同名字的文件,使用UUID来拼接扩展名形成不重复的文件名,利用传入的路径path
创建新的文件夹来存储文件,最后利用新的文件名和路径创建目标文件。通过MultipartFile
的transferTo
方法传输文件到目标文件夹,完成传输。
@Service("iFileService")
public class FileServiceImpl implements IFileService {
private static final Logger logger = LoggerFactory.getLogger(FileServiceImpl.class);
//上传文件
public String upload(MultipartFile file, String path){
//获取到文件的名称
String fileName = file.getOriginalFilename();
//扩展名
//将文件名分割出来得到扩展名
String fileExtensionsName = fileName.substring(fileName.lastIndexOf(".")+1);
//上传的文件名
//主要是为了避免多个用户上传相同文件名的文件
//A:abc.jpg
//B:abc.jpg
String uploadFileName = UUID.randomUUID().toString()+"."+fileExtensionsName;
//打印日志
logger.info("开始上传文件");
logger.info("开始上传文件,上传文件结束,文件名为:{},上传的路径为:{},新文件名为:{}",fileName,path,uploadFileName);
//文件夹
File fileDir = new File(path);
//如果不存在
if(!fileDir.exists()){
//改变文件可写
fileDir.setWritable(true);
//创建文件夹
fileDir.mkdirs();
}
//创建目标文件
File targetFile = new File(path,uploadFileName);
try {
//传输文件
file.transferTo(targetFile);
//代表传输文件成功 传输文件到FTP服务器
FTPUtil.uploadFile(Lists.newArrayList(targetFile));
//删除其中的文件
targetFile.delete();
} catch (IOException e) {
logger.error("上传文件异常",e);
}
return targetFile.getName();
}
}
上面的代码块中使用到了将文件传输到FTP
服务器的代码,将文件传输到FTP
服务器主要的目的是为了配合Nginx
来实现一个反向代理,从而我们可以通过Nginx
配置的相关的访问地址获取到存在FTP
服务器文件夹下的文件。
文件上传需要传入服务器的IP
地址,用户信息(用户名和密码),还需要端口号(默认为21), 以及FTPClient
这个功能类。涉及到用户名和密码的都需要登录(FTP
服务器的连接),先传入IP
获取连接,然后传入用户名和密码验证身份,连接成功。文件上传通过FTPClient
来实现,连接上以后,通过FTPClient
设置当前传输的相关属性,最后存入文件即可,文件通过FileInputStream
来实现传输。
public class FTPUtil {
private static final Logger logger = LoggerFactory.getLogger(FTPUtil.class);
//用获取参数的工具类获取到ip地址
private static String ftpIp = PropertiesUtil.getProperty("ftp.server.ip");
private static String ftpUser = PropertiesUtil.getProperty("ftp.user");
private static String ftpPass = PropertiesUtil.getProperty("ftp.pass");
//构造方法
public FTPUtil(String ip, int port, String user, String pwd){
this.ip = ip;
this.port = port;
this.user = user;
this.pwd = pwd;
}
public static boolean uploadFile(List<File> fileList) throws IOException {
FTPUtil ftpUtil = new FTPUtil(ftpIp,21,ftpUser,ftpPass);
logger.info("开始连接ftp服务器");
boolean result = ftpUtil.uploadFile("img",fileList);
logger.info("开始连接ftp服务器,结束上传,上传结果为:{}");
return result;
}
private boolean uploadFile(String remotePath, List<File> fileList) throws IOException {
boolean uploaded = true;
FileInputStream fis = null;
//开始连接FTP服务器
if(connectServer(this.ip,this.port,this.user,this.pwd)){
try {
//更改当前会话的工作目录
ftpClient.changeWorkingDirectory(remotePath);
//设置缓冲区
ftpClient.setBufferSize(1024);
//设置编码格式
ftpClient.setControlEncoding("UTF-8");
//设置文件的类型
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
//设置服务的被动端口范围
ftpClient.enterLocalPassiveMode();
//进行传输
for (File fileItem : fileList) {
//获取输入流
fis = new FileInputStream(fileItem);
//存储文件
ftpClient.storeFile(fileItem.getName(),fis);
}
} catch (IOException e) {
logger.error("上传文件异常",e);
uploaded = false;
e.printStackTrace();
} finally {
//记得关闭响应的流和服务器的断开连接
fis.close();
ftpClient.disconnect();
}
}
return uploaded;
}
private boolean connectServer(String ip, int port, String user, String pwd){
boolean isSuccess = false;
ftpClient = new FTPClient();
try {
//通过IP地址获取连接
ftpClient.connect(ip);
//验证身份
return ftpClient.login(user,pwd);
} catch (IOException e) {
logger.error("连接FTP服务器异常",e);
}
return false;
}
//FTP服务器连接的相关参数
private String ip;
private int port;
private String user;
private String pwd;
private FTPClient ftpClient;
}
//省略了相关参数的setter与getter方法
分页有开源项目,PageHelper分页助手
,可以很好的实现分页功能。一般的使用方法,传入需要分页的一页的容量pageNumber
和页码数pageSize
,利用PageHelper
获取开始分页。具体分页流程代码可以看出:集合List
的分页
public ServerResponse<PageInfo> getList(int pageNum, int pageSize){
//startPage -> start
PageHelper.startPage(pageNum,pageSize);
//填充自己的sql查询逻辑
//要获取到查询到的所有的商品
List<Product> productList = productMapper.selectList();
//Guaua中的集合类
List<ProductListVo> productListVoList = Lists.newArrayList();
for(Product productItem : productList){
//装配 ProductListVo是一个对product实现值装配的功能类
ProductListVo productListVo = assembleProductListVo(productItem);
productListVoList.add(productListVo);
}
//pageHelper -> 收尾
PageInfo pageResult = new PageInfo(productList);//会自动进行分页处理
pageResult.setList(productListVoList);
return ServerResponse.createBySuccess(pageResult);
}
如果需要接入支付宝付款的话,线下测试需要使用蚂蚁金服提供的沙箱环境,配置私钥和公钥,以及支付宝公钥,可以在蚂蚁金服开放平台蚂蚁金服开放中心的开发中心的研发服务中有沙箱,其中有相应的沙箱环境登录的账号和密码,私钥和公钥的生成使用的是RSA2
加密策略,可以下载密钥生成器,生成对应的私钥和公钥,然后复制公钥到沙箱环境中,生成相应的支付宝公钥,相关配置则成功。
支付模块的接入,首先从蚂蚁金服开放中心下载支付宝的SDK&Demo
文件,将其中的支付模块中的Main文件调通,调通的前提就是从其中获取到的二维码能够通过二维码生成器生成并且通过沙箱环境扫描支付生效。
实现以后,编写支付模块的代码。由于涉及到二维码图片的上传,需要从HttpServletRequest request
中获取到upload
的真实路径.
//获取文件的路径 相当于在webapp下建立一个upload的文件夹
String path = request.getSession().getServletContext().getRealPath("upload");
进入到服务层逻辑代码的编写。具体的除了需要传回去的订单编号以及所需要的上传到FTPServer
上的可以被Nginx
反向代理拿到的地址,其余都是按照支付宝支付流程,改变相关的逻辑。(主要的可以根据支付宝提供的Demo
文件中的Main
函数的public void test_trade_precreate()
代码来写)
public ServerResponse pay(Long orderNo, Integer userId, String path){
Map<String, String> resultMap = Maps.newHashMap();
//通过orderNo和userId来查询订单中是否有这个订单
Order order = orderMapper.selectOrderByOrderNoUserId(orderNo, userId);
if(order == null){
return ServerResponse.createByErrorMessage("用户没有此订单");
}
// 订单存在,放入订单号
resultMap.put("orderNo",String.valueOf(order.getOrderNo()));
// 走支付逻辑
// (必填) 商户网站订单系统中唯一订单号,64个字符以内,只能包含字母、数字、下划线,
// 需保证商户系统端不能重复,建议通过数据库sequence生成,
String outTradeNo = order.getOrderNo().toString();
// (必填) 订单标题,粗略描述用户的支付目的。如“xxx品牌xxx门店消费”
String subject = new StringBuilder().append("happymmall扫码支付,订单编号为:").append(order.getOrderNo().toString()).toString();
// (必填) 订单总金额,单位为元,不能超过1亿元
// 如果同时传入了【打折金额】,【不可打折金额】,【订单总金额】三者,则必须满足如下条件:【订单总金额】=【打折金额】+【不可打折金额】
String totalAmount = order.getPayment().toString();
// (可选) 订单不可打折金额,可以配合商家平台配置折扣活动,如果酒水不参与打折,则将对应金额填写至此字段
// 如果该值未传入,但传入了【订单总金额】,【打折金额】,则该值默认为【订单总金额】-【打折金额】
String undiscountableAmount = "0.0";
// 卖家支付宝账号ID,用于支持一个签约账号下支持打款到不同的收款账号,(打款到sellerId对应的支付宝账号)
// 如果该字段为空,则默认为与支付宝签约的商户的PID,也就是appid对应的PID
String sellerId = "";
// 订单描述,可以对交易或商品进行一个详细地描述,比如填写"购买商品3件共20.00元"
String body = new StringBuilder().append("订单").append(outTradeNo).append("购买商品共").append(totalAmount).toString();
// 商户操作员编号,添加此参数可以为商户操作员做销售统计
String operatorId = "test_operator_id";
// (必填) 商户门店编号,通过门店号和商家后台可以配置精准到门店的折扣信息,详询支付宝技术支持
String storeId = "test_store_id";
// 业务扩展参数,目前可添加由支付宝分配的系统商编号(通过setSysServiceProviderId方法),详情请咨询支付宝技术支持
ExtendParams extendParams = new ExtendParams();
extendParams.setSysServiceProviderId("2088100200300400500");
// 支付超时,定义为120分钟
String timeoutExpress = "120m";
// 商品明细列表,需填写购买商品详细信息,
List<GoodsDetail> goodsDetailList = new ArrayList<GoodsDetail>();
//获取订单中的所有的订单细节
List<OrderItem> orderItemList = orderItemMapper.selectOrderItemByOrderNoUserId(order.getOrderNo(),userId);
for(OrderItem orderItem : orderItemList){
// 创建一个商品信息,参数含义分别为商品id(使用国标)、名称、单价(单位为分)、数量,如果需要添加商品类别,详见GoodsDetail
GoodsDetail goods = GoodsDetail.newInstance(orderItem.getProductId().toString(),orderItem.getProductName(),
BigDecimalUtil.mul(orderItem.getCurrentUnitPrice().doubleValue(),100).longValue(),orderItem.getQuantity());
// 创建好一个商品后添加至商品明细列表
goodsDetailList.add(goods);
}
// 创建扫码支付请求builder,设置请求参数
AlipayTradePrecreateRequestBuilder builder = new AlipayTradePrecreateRequestBuilder()
.setSubject(subject).setTotalAmount(totalAmount).setOutTradeNo(outTradeNo)
.setUndiscountableAmount(undiscountableAmount).setSellerId(sellerId).setBody(body)
.setOperatorId(operatorId).setStoreId(storeId).setExtendParams(extendParams)
.setTimeoutExpress(timeoutExpress)
.setNotifyUrl(PropertiesUtil.getProperty("alipay.callback.url"))//支付宝服务器主动通知商户服务器里指定的页面http路径,根据需要设置
.setGoodsDetailList(goodsDetailList);
/** 一定要在创建AlipayTradeService之前调用Configs.init()设置默认参数
* Configs会读取classpath下的zfbinfo.properties文件配置信息,如果找不到该文件则确认该文件是否在classpath目录
*/
Configs.init("zfbinfo.properties");
/** 使用Configs提供的默认参数
* AlipayTradeService可以使用单例或者为静态成员对象,不需要反复new
*/
AlipayTradeService tradeService = new AlipayTradeServiceImpl.ClientBuilder().build();
AlipayF2FPrecreateResult result = tradeService.tradePrecreate(builder);
switch (result.getTradeStatus()) {
case SUCCESS:
logger.info("支付宝预下单成功: )");
AlipayTradePrecreateResponse response = result.getResponse();
dumpResponse(response);
//创建此路径的文件夹
File folder = new File(path);
//不存在就创建
if(!folder.exists()){
folder.setWritable(true);
folder.mkdirs();
}
//存在的话,传入
//细节 注意是path最后是没有/的,所以需要人为的加上
String qrPath = String.format(path+"/qr-%s.png",response.getOutTradeNo());
//后面的交易单号会替换%s
String qrFileName = String.format("qr-%s.png",response.getOutTradeNo());
//生成的二维码图片 -> 图片的路径为qrPath
ZxingUtils.getQRCodeImge(response.getQrCode(), 256, qrPath);
//第一个是parent,第二个是child
File targetFile = new File(path,qrFileName);
try {
//上传到FTP服务器
FTPUtil.uploadFile(Lists.newArrayList(targetFile));
} catch (IOException e) {
logger.error("上传二维码图片异常",e);
}
logger.info("qrPath",qrPath);
String qrUrl = PropertiesUtil.getProperty("ftp.server.http.prefix")+targetFile.getName();
//二维码的qrUrl路径
resultMap.put("qrUrl",qrUrl);
return ServerResponse.createBySuccess(resultMap);//最后前端获取到的就是订单编号和二维码图片
case FAILED:
logger.error("支付宝预下单失败!!!");
return ServerResponse.createByErrorMessage("支付宝预下单失败!!!");
case UNKNOWN:
logger.error("系统异常,预下单状态未知!!!");
return ServerResponse.createByErrorMessage("系统异常,预下单状态未知!!!");
default:
logger.error("不支持的交易状态,交易返回异常!!!");
return ServerResponse.createByErrorMessage("不支持的交易状态,交易返回异常!!!");
}
}
natap
是一种内网穿透的工具,也就是一种通过另一个地址可以访问你本机Tomcat
的手段技术。具体的使用natapp具体使用即可了解。
支付宝回调过程主要是为了获取扫描支付二维码前后的订单的一个支付状态的反馈以及订单中支付宝通过natapp
内网穿透远程调用Tomcat
中的URL
来调用回调功能代码。由于使用alipay
支付以后所有的数据都在请求回调的请求中,通过request
获取到Map
集合数据,通过迭代器获取到每一个值,并通过Map
保存起来。然后需要通过支付宝的验签步骤。
public Object alipayCallback(HttpServletRequest request){
Map<String, String> params = Maps.newHashMap();
//所有的内容都放在请求中
Map<String, String[]> requestParams = request.getParameterMap();
//使用迭代器
for(Iterator iterator = requestParams.keySet().iterator(); iterator.hasNext();){
String name = (String)iterator.next();
String[] values = requestParams.get(name);
//对于值做一个拼接
String valueStr = "";
for (int i = 0; i < values.length; i++) {
//判断是否是最后一个元素,以判断是否需要使用逗号拼接
valueStr = (i == values.length - 1) ? valueStr+values[i] : valueStr+values[i]+",";
}
//从request中获取到了数据
params.put(name,valueStr);
}
logger.info("支付宝回调,sign:{},trade_status:{},参数:{}",params.get("sign"),params.get("trade_status"),params.toString());
//验证回调的正确性,验证是否是支付宝发送的,同时要避免重复通知
//首先根据要求移除sign、sign_type,由于sign支付宝自己移除了
params.remove("sign_type");
try {
//这里注意获取的一定是支付宝的公钥,否则验签一定失败!!!
boolean alipayRSTCheckedV2 = AlipaySignature.rsaCheckV2(params,Configs.getAlipayPublicKey(),"utf-8",Configs.getSignType());
if(!alipayRSTCheckedV2){
return ServerResponse.createByErrorMessage("回调请求异常,错误请求再次发送直接转网警");
}
} catch (AlipayApiException e) {
logger.error("支付宝回调异常");
}
//如果支付宝能够完成回调的逻辑,那么可以表示此订单没有问题,可以执行验证订单是否支付
//逻辑代码 验证订单是否支付
ServerResponse serverResponse = iOrderService.alipayCallback(params);
if(serverResponse.isSuccess()){
//这里返回的是success,因为支付宝接收不到success,会重复通知,直至接收到success
return Const.AlipayCallback.RESPONSE_SUCCESS;
}
//此处是failed,与上相对
return Const.AlipayCallback.RESPONSE_FAILED;
}
其中的回调服务层逻辑代码功能:从封装的参数中获取到商户订单号,支付宝交易号,交易状态,根据订单的状态以及支付的状态,判定此订单的状态,同时确定否要更新订单信息。最后写入付款明细。
public ServerResponse alipayCallback(Map<String, String> params){
//从参数中获取商户订单编号
Long orderNo = Long.valueOf(params.get("out_trade_no"));
//支付宝交易号
String tradeNo = params.get("trade_no");
//支付宝交易状态
String tradeStatus = params.get("trade_status");
//在order中查询此订单号是否有订单
Order order = orderMapper.selectOrderByOrderNo(orderNo);
if(order == null){
return ServerResponse.createByErrorMessage("此订单不是该系统订单,回调忽略");
}
//订单存在的话,需要看其支付状态
if(order.getStatus() >= Const.OrderStatusEnum.PAID.getCode()){
//因为返回过去的值需要判断是否success
return ServerResponse.createBySuccess("支付宝重复调用");
}
//判断订单的状态
if(Const.AlipayCallback.TRADE_STATUS_TRADE_SUCCESS.equals(tradeStatus)){
/** 这里订单的时间需要设置一下
*/
order.setPaymentTime(DateTimeUtil.strToDate(params.get("gmt_payment")));
//订单交易支付成功,改变订单状态
order.setStatus(Const.OrderStatusEnum.PAID.getCode());
//更新订单表
orderMapper.updateByPrimaryKeySelective(order);
}
//付款明细
PayInfo payInfo = new PayInfo();
payInfo.setOrderNo(order.getOrderNo());
payInfo.setUserId(order.getUserId());
payInfo.setPayPlatform(Const.PayPlatFormEnum.ALIPAY.getCode());
payInfo.setPlatformNumber(tradeNo);
payInfo.setPlatformStatus(tradeStatus);
payInfoMapper.insert(payInfo);
return ServerResponse.createBySuccess();
}
这里的回调是通过natapp
提供的http
地址来访问的,具体的通知url
在支付逻辑中设定了。