Commit 7008142a authored by Administrator's avatar Administrator

创建项目

parent 9398f372
......@@ -16,6 +16,18 @@
<dependencies>
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-client</artifactId>
<version>1.8.0</version>
<exclusions>
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>cn.quant.baa.pay</groupId>
<artifactId>baa-pay-api</artifactId>
......
package cn.quant.baa.pay;
import java.time.Duration;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
......@@ -17,4 +18,9 @@ public class Constant {
*/
public static final LocalDate MINI_LOCAL_DATE = LocalDate.of(1970, 1, 1);
public static final Date MINI_DATE = Date.from(MINI_LOCAL_DATE.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
public static final String REDIS_NAMESPACE_PAY = "BAAPAY:PAY:P";
@Deprecated
public static final Duration PAY_DUE_TIME = Duration.ofMinutes(30);
}
package cn.quant.baa.pay.acquirer;
import cn.quant.baa.pay.jpa.entity.PayHistoryEntity;
import com.fasterxml.jackson.databind.JsonNode;
/**
......@@ -8,6 +9,8 @@ import com.fasterxml.jackson.databind.JsonNode;
public class ChannelResponse {
private boolean success;
private String orderNo;
private String code;
private String message;
......@@ -22,9 +25,7 @@ public class ChannelResponse {
//微信平台签名
private String signature;
private String payUrl;
private String prepayId;
private String notification;
private JsonNode node;
......@@ -36,6 +37,14 @@ public class ChannelResponse {
this.success = success;
}
public String getOrderNo() {
return orderNo;
}
public void setOrderNo(String orderNo) {
this.orderNo = orderNo;
}
public String getCode() {
return code;
}
......@@ -92,20 +101,12 @@ public class ChannelResponse {
this.signature = signature;
}
public String getPayUrl() {
return payUrl;
}
public void setPayUrl(String payUrl) {
this.payUrl = payUrl;
}
public String getPrepayId() {
return prepayId;
public String getNotification() {
return notification;
}
public void setPrepayId(String prepayId) {
this.prepayId = prepayId;
public void setNotification(String notification) {
this.notification = notification;
}
public JsonNode getNode() {
......@@ -127,8 +128,7 @@ public class ChannelResponse {
sb.append(", timestamp='").append(timestamp).append('\'');
sb.append(", nonce='").append(nonce).append('\'');
sb.append(", signature='").append(signature).append('\'');
sb.append(", payUrl='").append(payUrl).append('\'');
sb.append(", prepayId='").append(prepayId).append('\'');
sb.append(", notification='").append(notification).append('\'');
sb.append(", node=").append(node);
sb.append('}');
return sb.toString();
......
......@@ -64,27 +64,29 @@ public class MerchantAcquirer implements Acquirer {
}
@Override
public ChannelResponse pay(PayRequestData payRequestData, PayHistoryEntity payHistoryEntity) throws Exception {
return acquirers.get(payRequestData.getChanId()).pay(payRequestData, payHistoryEntity);
public ChannelResponse pay(PayRequestData request, PayHistoryEntity history) throws Exception {
ChannelResponse response = acquirers.get(request.getChanId()).pay(request, history);
response.setOrderNo(String.valueOf(history.getTransactionId()));
return response;
}
@Override
public ChannelResponse refund(RefundRequestData refundRequestData) throws Exception {
return acquirers.get(refundRequestData.getChanId()).refund(refundRequestData);
public ChannelResponse refund(RefundRequestData request) throws Exception {
return acquirers.get(request.getChanId()).refund(request);
}
@Override
public ChannelResponse checkPay(CheckPayRequestData checkPayRequestData) throws Exception {
return acquirers.get(checkPayRequestData.getChanId()).checkPay(checkPayRequestData);
public ChannelResponse checkPay(CheckPayRequestData request) throws Exception {
return acquirers.get(request.getChanId()).checkPay(request);
}
@Override
public ChannelResponse checkRefund(CheckRefundRequestData checkRefundRequestData) throws Exception {
return acquirers.get(checkRefundRequestData.getChanId()).checkRefund(checkRefundRequestData);
public ChannelResponse checkRefund(CheckRefundRequestData request) throws Exception {
return acquirers.get(request.getChanId()).checkRefund(request);
}
@Override
public ChannelResponse close(CloseRequestData closeRequestData) throws Exception {
return acquirers.get(closeRequestData.getChanId()).close(closeRequestData);
public ChannelResponse close(CloseRequestData request) throws Exception {
return acquirers.get(request.getChanId()).close(request);
}
}
......@@ -100,15 +100,15 @@ public class WeiXinMerchantAcquirer extends MerchantAcquirer {
}
@Override
public ChannelResponse pay(PayRequestData payRequestData, PayHistoryEntity history) throws Exception {
public ChannelResponse pay(PayRequestData request, PayHistoryEntity history) throws Exception {
ObjectNode bodyNode = objectMapper.createObjectNode();
// 转换金额为分
BigInteger amount = new BigDecimal(payRequestData.getAmount()).multiply(new BigDecimal(100)).toBigInteger();
BigInteger amount = new BigDecimal(request.getAmount()).multiply(new BigDecimal(100)).toBigInteger();
bodyNode.put("mchid", properties.getPayAcctId())
.put("appid", properties.getPayAppId())
.put("attach", payRequestData.getAttach())
.put("description", payRequestData.getSubject())
.put("notify_url", payRequestData.getNotifyUrl())
.put("attach", request.getAttach())
.put("description", request.getSubject())
.put("notify_url", request.getNotifyUrl())
.put("out_trade_no", history.getTransactionId().toString());
bodyNode.putObject("amount")
.put("total", amount);
......@@ -127,16 +127,16 @@ public class WeiXinMerchantAcquirer extends MerchantAcquirer {
}
bodyNode.set("scene_info", sceneInfo);
String payAccess = properties.getPayAccess();
ChannelResponse response = doExecute(EXECUTE_PAY, payAccess, history.getIds(), bodyNode);
ChannelResponse response = doExecute(EXECUTE_PAY, payAccess, request, bodyNode);
JsonNode node = response.getNode();
if (node != null) {
node = response.getNode().get("h5_url");
if (node != null) {
response.setPayUrl(node.asText());
response.setNotification(node.asText());
}
node = response.getNode().get("prepay_id");
if (node != null) {
response.setPrepayId(node.asText());
response.setNotification(node.asText());
}
//TODO:he APP、小程序、H5、JS不同的方式设置对应的返回参数
}
......@@ -187,7 +187,7 @@ public class WeiXinMerchantAcquirer extends MerchantAcquirer {
* @param bodyNode
* @return
*/
private ChannelResponse doExecute(String execute, String access, PayHistoryIds ids, ObjectNode bodyNode) throws Exception {
private ChannelResponse doExecute(String execute, String access, Object request, ObjectNode bodyNode) throws Exception {
ChannelResponse response = null;
......@@ -238,7 +238,7 @@ public class WeiXinMerchantAcquirer extends MerchantAcquirer {
// if (null == body) {
// throw new NullException("http response body is null.");
// } else {
response = response(execute, access, ids, clientResponse);
response = response(execute, access, request, clientResponse);
//签名信息
// String wsSignText = joining("\n", wxTimestamp, nonce, body);
......@@ -267,17 +267,17 @@ public class WeiXinMerchantAcquirer extends MerchantAcquirer {
response.setCode(COMMON_ERROR_CODE);
response.setMessage(COMMON_ERROR_CODE);
} else {
response.setCode(itemDTO.getCode());
response.setCode(itemDTO.getValue());
response.setMessage(itemDTO.getText());
}
}
return response;
}
private ChannelResponse response(String execute, String access, PayHistoryIds ids, ClientResponse response) {
private ChannelResponse response(String execute, String access, Object request, ClientResponse response) {
if (response == null) {
throw new NullException(StringUtils.format("Client response is null : {}; {}; {};", execute, access, ids));
throw new NullException(StringUtils.format("Client response is null : {}; {}; {};", execute, access, request));
}
ChannelResponse responseData = new ChannelResponse();
......@@ -314,18 +314,18 @@ public class WeiXinMerchantAcquirer extends MerchantAcquirer {
} else {
responseData.setSuccess(false);
responseData.setCode(codeNode.asText(COMMON_ERROR_CODE));
responseData.setMessage(StringUtils.format("Client response http status : {}; {}; {}; {};", execute, access, ids, httpStatus));
responseData.setMessage(StringUtils.format("Client response http status : {}; {}; {}; {};", execute, access, request, httpStatus));
logger.warn(responseData.getMessage());
}
} else {
responseData.setSuccess(false);
responseData.setCode(codeNode.asText(COMMON_ERROR_CODE));
responseData.setMessage(jsonNode.get("message").asText(""));
logger.warn("Client response body code : {}; {}; {}; {};", execute, access, ids, responseData);
logger.warn("Client response body code : {}; {}; {}; {};", execute, access, request, responseData);
}
} catch (Exception e) {
logger.warn(StringUtils.format("Object mapper read response : {}; {}; {};", execute, access, ids), e);
logger.warn(StringUtils.format("Object mapper read response : {}; {}; {};", execute, access, request), e);
responseData.setSuccess(false);
responseData.setCode(COMMON_ERROR_CODE);
responseData.setMessage(e.getMessage());
......
......@@ -48,5 +48,6 @@ public class ServerConfiguration {
PersistentEntityInvoker invoker = new PersistentEntityInvoker(repositories);
return invoker;
}
}
......@@ -4,7 +4,7 @@ package cn.quant.baa.pay.dict;
* Created by Administrator on 2021/8/24 0024.
*/
public enum MessageEnum {
EMPTY_REQ_DATA("2000000"),
ILLEGAL_REQ_DATA("2000000"),
ILLEGAL_REQ_SUBJECT("2000000"),
ILLEGAL_REQ_MCH_ID("2000000"),
ILLEGAL_REQ_CHAN_ID("2000000"),
......
......@@ -7,14 +7,11 @@ import cn.quant.baa.pay.jpa.entity.*;
import cn.quant.baa.pay.model.web.GoodsDetail;
import cn.quant.baa.pay.model.web.PayRequestData;
import cn.quant.spring.util.DateUtils;
import org.hibernate.engine.jdbc.batch.spi.Batch;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
import static cn.quant.baa.pay.Constant.MINI_DATE;
import static cn.quant.baa.pay.Constant.MINI_LOCAL_DATE;
/**
......@@ -63,7 +60,7 @@ public class EntityBuilder {
return account;
}
public static PayHistoryEntity payHistory(AccountEntity account, PayHistoryIds ids, long historyId, PayRequestData data, AcquirerProperties profile
public static PayHistoryEntity payHistory(AccountEntity account, PayHistoryIds ids, long historyId, PayRequestData data, AcquirerProperties properties
, TransactionSession session) {
BigDecimal discount = new BigDecimal(data.getDiscounts());
......@@ -81,22 +78,17 @@ public class EntityBuilder {
entity.setCreditDebitFlag(CreditDebitFlag.C);
entity.setCurrencyCode(CurrencyCode.CNY);
entity.setCustomerGenFlag(true);
entity.setChannelId(properties.getMchChanId());
entity.setDiscAmount(discount);
entity.setGenFeeAmount(BigDecimal.ZERO);
entity.setGoodsSigner(signer);
entity.setShopName(data.getShopName());
entity.setMobilePhone(data.getPhoneNo());
entity.setNotifyUrl(data.getNotifyUrl());
entity.setOriginalTxnAmount(origAmt);
entity.setPayDueTime(LocalDateTime.now().plusMinutes(30));
entity.setPayAcctId(profile.getPayAcctId());
entity.setPayChanCode(profile.getPayChanCode());
entity.setPayAppId(profile.getPayAppId());
entity.setPayMethod(PayMethod.DRST);
entity.setPostCashAmount(cash);
entity.setPostCreditAmount(credit);
entity.setPartitionKey(account.getPartitionKey());
entity.setRequestId(session.getRequestId());
entity.setRequestTime(session.getRequestTime());
entity.setShopName(data.getShopName());
entity.setStatementeFlag(true);
......@@ -139,7 +131,7 @@ public class EntityBuilder {
return entity;
}
public static BatchCycleTriggerEntity payTrigger(Long triggerId, PayHistoryEntity history){
public static BatchCycleTriggerEntity payTrigger(Long triggerId, PayHistoryEntity history) {
Date now = DateUtils.now();
BatchCycleTriggerEntity entity = new BatchCycleTriggerEntity();
entity.setAccountId(history.getAccountId());
......
......@@ -36,6 +36,9 @@ public class BatchCycleTriggerEntity extends PartitionEntity implements Serializ
@Column(name = "PAY_DUE_TIME", nullable = false)
private LocalDateTime payDueTime;
@Column(name = "NOTIFY_URL", nullable = true, length = 1000)
private String notifyUrl;
@Enumerated(EnumType.STRING)
@Column(name = "CHAN_NOTIFY_STATUS", nullable = false, length = 4)
private StatusCode chanNotifyStatus;
......@@ -50,6 +53,9 @@ public class BatchCycleTriggerEntity extends PartitionEntity implements Serializ
@Column(name = "MCH_NOTIFY_TIME", nullable = true)
private Date mchNotifyTime;
@Column(name = "REQUEST_ID", nullable = true, length = 64)
private String requestId;
public long getTriggerId() {
return triggerId;
}
......@@ -90,6 +96,14 @@ public class BatchCycleTriggerEntity extends PartitionEntity implements Serializ
this.payDueTime = payDueTime;
}
public String getNotifyUrl() {
return notifyUrl;
}
public void setNotifyUrl(String notifyUrl) {
this.notifyUrl = notifyUrl;
}
public StatusCode getChanNotifyStatus() {
return chanNotifyStatus;
}
......@@ -122,6 +136,14 @@ public class BatchCycleTriggerEntity extends PartitionEntity implements Serializ
this.mchNotifyTime = mchNotifyTime;
}
public String getRequestId() {
return requestId;
}
public void setRequestId(String requestId) {
this.requestId = requestId;
}
@Override
public String getPersistentKey() {
return StringUtils.toDelimitedString(CLASS_NAME, triggerId);
......@@ -143,10 +165,12 @@ public class BatchCycleTriggerEntity extends PartitionEntity implements Serializ
sb.append(", targetId=").append(targetId);
sb.append(", accountId=").append(accountId);
sb.append(", payDueTime=").append(payDueTime);
sb.append(", chanNotifyStatus='").append(chanNotifyStatus).append('\'');
sb.append(", notifyUrl='").append(notifyUrl).append('\'');
sb.append(", chanNotifyStatus=").append(chanNotifyStatus);
sb.append(", chanNotifyTime=").append(chanNotifyTime);
sb.append(", mchNotifyStatus='").append(mchNotifyStatus).append('\'');
sb.append(", mchNotifyStatus=").append(mchNotifyStatus);
sb.append(", mchNotifyTime=").append(mchNotifyTime);
sb.append(", requestId='").append(requestId).append('\'');
sb.append(',').append(super.toString());
sb.append('}');
return sb.toString();
......
......@@ -33,14 +33,8 @@ public class PayHistoryEntity extends DescribablePartitionEntity implements Seri
@Column(name = "ACCOUNT_ID", nullable = false, updatable = false)
private Long accountId;
@Column(name = "PAY_APP_ID", nullable = false, updatable = false, length = 32)
private String payAppId;
@Column(name = "PAY_CHAN_CODE", nullable = false, updatable = false, length = 4)
private String payChanCode;
@Column(name = "PAY_ACCT_ID", nullable = false, updatable = false, length = 4)
private String payAcctId;
@Column(name = "CHANNEL_ID", nullable = false, updatable = false)
private Long channelId;
@Column(name = "SUBJECT", nullable = false, updatable = false, length = 128)
private String subject;
......@@ -125,9 +119,6 @@ public class PayHistoryEntity extends DescribablePartitionEntity implements Seri
@Column(name = "ADDRESS", nullable = true, length = 500)
private String address;
@Column(name = "NOTIFY_URL", nullable = true, length = 1000)
private String notifyUrl;
@Column(name = "ATTACH_TEXT", nullable = true, length = 512)
private String attachText;
......@@ -155,28 +146,12 @@ public class PayHistoryEntity extends DescribablePartitionEntity implements Seri
this.accountId = accountId;
}
public String getPayAppId() {
return payAppId;
}
public void setPayAppId(String payAppId) {
this.payAppId = payAppId;
}
public String getPayChanCode() {
return payChanCode;
public Long getChannelId() {
return channelId;
}
public void setPayChanCode(String payChanCode) {
this.payChanCode = payChanCode;
}
public String getPayAcctId() {
return payAcctId;
}
public void setPayAcctId(String payAcctId) {
this.payAcctId = payAcctId;
public void setChannelId(Long channelId) {
this.channelId = channelId;
}
public String getSubject() {
......@@ -387,14 +362,6 @@ public class PayHistoryEntity extends DescribablePartitionEntity implements Seri
this.address = address;
}
public String getNotifyUrl() {
return notifyUrl;
}
public void setNotifyUrl(String notifyUrl) {
this.notifyUrl = notifyUrl;
}
public String getAttachText() {
return attachText;
}
......@@ -422,9 +389,7 @@ public class PayHistoryEntity extends DescribablePartitionEntity implements Seri
sb.append("ids=").append(ids);
sb.append(", transactionId=").append(transactionId);
sb.append(", accountId=").append(accountId);
sb.append(", payAppId='").append(payAppId).append('\'');
sb.append(", payChanCode='").append(payChanCode).append('\'');
sb.append(", payAcctId='").append(payAcctId).append('\'');
sb.append(", channelId=").append(channelId);
sb.append(", subject='").append(subject).append('\'');
sb.append(", statusCode=").append(statusCode);
sb.append(", currencyCode=").append(currencyCode);
......@@ -451,7 +416,6 @@ public class PayHistoryEntity extends DescribablePartitionEntity implements Seri
sb.append(", mobilePhone='").append(mobilePhone).append('\'');
sb.append(", goodsSigner='").append(goodsSigner).append('\'');
sb.append(", address='").append(address).append('\'');
sb.append(", notifyUrl='").append(notifyUrl).append('\'');
sb.append(", attachText='").append(attachText).append('\'');
sb.append(',').append(super.toString());
sb.append('}');
......
package cn.quant.baa.pay.model.web;
/**
* Created by Administrator on 2021/9/24 0024.
*/
public interface ChannelRequestData {
String getChanId();
}
......@@ -9,7 +9,7 @@ import java.util.Arrays;
/**
* Created by Administrator on 2021/8/24 0024.
*/
public class PayRequestData implements Serializable{
public class PayRequestData implements ChannelRequestData, Serializable{
private static final long serialVersionUID = -8274612481658761761L;
......@@ -25,10 +25,6 @@ public class PayRequestData implements Serializable{
@NotNull(message = "ILLEGAL_REQ_MCH_ID")
private String mchId;
/**
* 支付通道ID
*/
@NotNull(message = "ILLEGAL_REQ_CHAN_ID")
private String chanId;
/**
......
package cn.quant.baa.pay.model.web;
/**
* Created by Administrator on 2021/9/24 0024.
*/
public class PayResponseData {
private String orderNo;
private String prepay;
public String getOrderNo() {
return orderNo;
}
public void setOrderNo(String orderNo) {
this.orderNo = orderNo;
}
public String getPrepay() {
return prepay;
}
public void setPrepay(String prepay) {
this.prepay = prepay;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("PayResponseData{");
sb.append("orderNo='").append(orderNo).append('\'');
sb.append(", prepay='").append(prepay).append('\'');
sb.append('}');
return sb.toString();
}
}
EMPTY_REQ_DATA=Illegal request data.
ILLEGAL_REQ_DATA=Illegal request data.
ILLEGAL_REQ_MCH_ID=Illegal merchant id - {0}.
ILLEGAL_REQ_CHAN_ID=Illegal pay channel id - {0}.
ILLEGAL_REQ_BUYER_ID=Illegal payer id - {0}.
......
EMPTY_REQ_DATA=Illegal request data.
ILLEGAL_REQ_DATA=Illegal request data.
ILLEGAL_REQ_MCH_ID=Illegal merchant id - {0}.
ILLEGAL_REQ_CHAN_ID=Illegal pay channel id - {0}.
ILLEGAL_REQ_BUYER_ID=Illegal payer id - {0}.
......
EMPTY_REQ_DATA=\u8BF7\u6C42\u6570\u636E\u683C\u5F0F\u9519\u8BEF
ILLEGAL_REQ_DATA=\u8BF7\u6C42\u6570\u636E"data"\u683C\u5F0F\u9519\u8BEF
ILLEGAL_REQ_SUBJECT={0}-\u8BA2\u5355\u6807\u9898\u9519\u8BEF
ILLEGAL_REQ_MCH_ID={0}-\u5546\u6237ID\u9519\u8BEF
ILLEGAL_REQ_CHAN_ID={0}-\u652F\u4ED8\u901A\u9053ID\u9519\u8BEF
......
......@@ -2,6 +2,7 @@ package cn.quant.baa.pay;
import cn.quant.spring.util.IdentitySequencer;
import cn.quant.spring.util.ServerUtils;
import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
......@@ -21,10 +22,11 @@ import java.time.format.DateTimeFormatter;
/**
* <p><b>Application VM Option:</b><br/>
* <code>--spring.config.file=${project.build.directory}/target/config/application-dev.properties </code><br/>
* <code>-Dlogging.config=${project.build.directory}/target/config/logback-spring.xml</code></p>
* <code>-Dlogging.config=${project.build.directory}/target/config/logback-pro.xml</code></p>
* <p><b>Enable Dubbo Annotation: </b><br/>
* <code>@org.springframework.context.annotation.ImportResource(locations = {"classpath:application-dubbo.xml"})</code></p>
*/
@EnableApolloConfig
@ComponentScan
@SpringBootApplication
@EnableAutoConfiguration
......
package cn.quant.baa.pay.rest;
import cn.quant.baa.pay.acquirer.ChannelResponse;
import cn.quant.baa.pay.context.TransactionSession;
import cn.quant.baa.pay.model.web.PayResponseData;
import cn.quant.spring.http.HttpResponseData;
import cn.quant.spring.rest.AbstractController;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.ZonedDateTimeSerializer;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.Map;
/**
* Created by Administrator on 2021/8/28 0028.
*/
public abstract class BusinessController extends AbstractController {
private final static ObjectMapper objectMapper = new ObjectMapper();
public BusinessController() {
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
simpleModule.addSerializer(LocalDateTime.class, LocalDateTimeSerializer.INSTANCE);
simpleModule.addDeserializer(LocalDateTime.class, LocalDateTimeDeserializer.INSTANCE);
simpleModule.addSerializer(LocalDate.class, LocalDateSerializer.INSTANCE);
simpleModule.addDeserializer(LocalDate.class, LocalDateDeserializer.INSTANCE);
simpleModule.addSerializer(ZonedDateTime.class, ZonedDateTimeSerializer.INSTANCE);
objectMapper.registerModule(simpleModule);
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
}
public Map deserialize(String value) throws IOException {
return objectMapper.readValue(value.getBytes(), Map.class);
}
public String serialize(HttpResponseData value) throws JsonProcessingException {
return objectMapper.writeValueAsString(value);
}
public HttpResponseData response(ChannelResponse response) {
TransactionSession session = TransactionSession.session();
PayResponseData data = new PayResponseData();
data.setOrderNo(response.getOrderNo());
data.setPrepay(response.getNotification());
HttpResponseData responseData = new HttpResponseData(data);
responseData.setSuccess(response.getSuccess());
responseData.setMessage(response.getMessage());
responseData.setStatus(response.getCode());
responseData.setRequestId(session.getRequestId());
return responseData;
}
}
......@@ -45,7 +45,7 @@ public class MerchantController extends AbstractController {
MerchantChannelRequestData data = request.getData();
if (data == null) {
AssertUtils.throwMessage(EMPTY_REQ_DATA);
AssertUtils.throwMessage(ILLEGAL_REQ_DATA);
}
Map<String, MerchantChannelResponseData> channels = merchantStorageService.getChannels(data);
......@@ -63,7 +63,7 @@ public class MerchantController extends AbstractController {
MerchantChannelRequestData data = request.getData();
if (data == null) {
AssertUtils.throwMessage(EMPTY_REQ_DATA);
AssertUtils.throwMessage(ILLEGAL_REQ_DATA);
}
MerchantAcquirerProperties channel = merchantStorageService.getChannel(Long.parseLong(data.getChanId()));
......
//package cn.quant.baa.pay.rest;
//
//import cn.quant.baa.pay.annotation.BusinessMapping;
//import cn.quant.baa.pay.component.Sequencer;
//import cn.quant.baa.pay.context.TransactionSession;
//import cn.quant.baa.pay.model.BusinessRequest;
//import cn.quant.baa.pay.model.web.CheckPayRequestData;
//import cn.quant.baa.pay.model.web.CheckRefundRequestData;
//import cn.quant.baa.pay.model.web.PayRequestData;
//import cn.quant.baa.pay.service.TransactionService;
//import cn.quant.spring.context.ServerApplicationContext;
//import com.fasterxml.jackson.core.JsonProcessingException;
//import com.fasterxml.jackson.databind.DeserializationFeature;
//import com.fasterxml.jackson.databind.JsonNode;
//import com.fasterxml.jackson.databind.ObjectMapper;
//import com.fasterxml.jackson.databind.PropertyNamingStrategy;
//import com.fasterxml.jackson.databind.json.JsonMapper;
//import com.fasterxml.jackson.databind.node.ObjectNode;
//import org.json.JSONObject;
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.stereotype.Controller;
//import org.springframework.ui.Model;
//import org.springframework.web.bind.annotation.*;
//
//import javax.servlet.http.HttpServletRequest;
//import java.util.HashMap;
//import java.util.Map;
//
///**
// * Created with IntelliJ IDEA.
// * Author: Lipeng Liu
// * Date: 2021/9/9
// * Time: 上午9:51
// * Description: No Description
// */
//@Controller
////@RestController
//@RequestMapping("pay")
//public class PayTestController {
//
// ObjectMapper objectMapper = new ObjectMapper();
//
// @Autowired
// private TransactionService transactionService;
//
// @Autowired
// private ServerApplicationContext serverApplicationContext;
//
// @Autowired
// private Sequencer sequencer;
//
// @GetMapping("")
// public String getPay(Model model) {
// return "pay/index";
// }
//
// @RequestMapping("h5")
// public String getH5(Model model) {
// return "pay/h5";
// }
//
// @RequestMapping("checkPay")
// public String checkPay(CheckPayRequestData data, Model model, HttpServletRequest servletRequest) {
// String res = "";
// if (servletRequest.getMethod().equals("POST")) {
// res = transactionService.checkPay(data).toString();
// }
// model.addAttribute("res", res);
// return "pay/checkPay";
// }
//
// @RequestMapping("checkRefund")
// public String checkRefund(CheckRefundRequestData data, Model model, HttpServletRequest servletRequest) {
//
// String res = "";
// if (servletRequest.getMethod().equals("POST")) {
// res = transactionService.checkRefund(data).toString();
// }
// model.addAttribute("res", res);
// return "pay/checkRefund";
// }
//
//
package cn.quant.baa.pay.rest;
import cn.quant.baa.pay.acquirer.ChannelResponse;
import cn.quant.baa.pay.annotation.BusinessMapping;
import cn.quant.baa.pay.component.Sequencer;
import cn.quant.baa.pay.model.BusinessRequest;
import cn.quant.baa.pay.model.web.CheckPayRequestData;
import cn.quant.baa.pay.model.web.CheckRefundRequestData;
import cn.quant.baa.pay.model.web.PayRequestData;
import cn.quant.baa.pay.service.TransactionService;
import cn.quant.spring.context.ServerApplicationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
/**
* Created with IntelliJ IDEA.
* Author: Lipeng Liu
* Date: 2021/9/9
* Time: 上午9:51
* Description: No Description
*/
@Controller
//@RestController
@RequestMapping("pay")
public class PayTestController {
ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private TransactionService transactionService;
@Autowired
private ServerApplicationContext serverApplicationContext;
@Autowired
private Sequencer sequencer;
@GetMapping("")
public String getPay(Model model) {
return "pay/index";
}
@RequestMapping("h5")
public String getH5(Model model) {
return "pay/h5";
}
@RequestMapping("checkPay")
public String checkPay(CheckPayRequestData data, Model model, HttpServletRequest servletRequest) throws Exception {
String res = "";
if (servletRequest.getMethod().equals("POST")) {
res = transactionService.checkPay(data).toString();
}
model.addAttribute("res", res);
return "pay/checkPay";
}
@RequestMapping("checkRefund")
public String checkRefund(CheckRefundRequestData data, Model model, HttpServletRequest servletRequest) throws Exception {
String res = "";
if (servletRequest.getMethod().equals("POST")) {
res = transactionService.checkRefund(data).toString();
}
model.addAttribute("res", res);
return "pay/checkRefund";
}
// @PostMapping("goPay")
// @BusinessMapping(session = 1)
// @ResponseBody
// public JsonNode goPay(@RequestBody(required = false) BusinessRequest<PayRequestData> requestData) {
// public ChannelResponse goPay(@RequestBody(required = false) BusinessRequest<PayRequestData> requestData) throws Exception {
// PayRequestData data = requestData.getData();
// String str = "{\"subject\":\"测试订单1\",\"mchId\":\"wx2f44c7fe7b08458d\",\"chanId\":\"75772285618946307\",\"outTradeNo\":\"11111111223\",\"originalAmount\":\"110.00\",\"amount\":0.01,\"discounts\":\"10.00\",\"notifyUrl\":\"http://127.0.0.1:8080/notifyUrl\",\"buyerId\":\"777777\",\"attach\":\"AAAA-BBBB-1111-2222\",\"creditAmount\":\"10.00\",\"cashAmount\":\"10\",\"goodsDetail\":[{\"goodsNo\":\"123123\",\"goodsId\":\"11111\",\"goodsName\":\"商品1\",\"quantity\":2,\"price\":\"10.00\",\"discounts\":\"2.5\",\"amount\":\"17.5\",\"attach\":\"---\",\"creditAmount\":123,\"cashAmount\":123}]}";
// try {
// objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// PayRequestData payRequestData = objectMapper.readValue(str, PayRequestData.class);
// String[] temp = data.getChanId().split("_");
// payRequestData.setChanId(temp[0]);
// payRequestData.setMchId(temp[1]);
// payRequestData.setOutTradeNo(data.getOutTradeNo());
// payRequestData.setSubject(data.getSubject());
// payRequestData.setAmount(data.getAmount());
// payRequestData.setNotifyUrl("http://127.0.0.1:8080/notifyUrl");
// JsonNode res = transactionService.pay(payRequestData);
// return res;
// } catch (JsonProcessingException e) {
// e.printStackTrace();
// }
// return objectMapper.createObjectNode();
// }
//
//}
\ No newline at end of file
// objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// PayRequestData payRequestData = objectMapper.readValue(str, PayRequestData.class);
// String[] temp = data.getChanId().split("_");
// payRequestData.setChanId(temp[0]);
// payRequestData.setMchId(temp[1]);
// payRequestData.setOutTradeNo(data.getOutTradeNo());
// payRequestData.setSubject(data.getSubject());
// payRequestData.setAmount(data.getAmount());
// payRequestData.setNotifyUrl("http://127.0.0.1:8080/notifyUrl");
// ChannelResponse response = transactionService.pay(payRequestData);
// return response;
//
// }
}
\ No newline at end of file
package cn.quant.baa.pay.rest;
import cn.quant.baa.pay.acquirer.AcquirerProperties;
import cn.quant.baa.pay.acquirer.AcquirerPropertiesSource;
import cn.quant.baa.pay.acquirer.ChannelResponse;
import cn.quant.baa.pay.annotation.BusinessMapping;
import cn.quant.baa.pay.model.BusinessRequest;
import cn.quant.baa.pay.model.web.*;
import cn.quant.baa.pay.service.TransactionService;
import cn.quant.baa.pay.util.AssertUtils;
import cn.quant.spring.http.HttpResponseData;
import cn.quant.spring.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import static cn.quant.baa.pay.Constant.PAY_DUE_TIME;
import static cn.quant.baa.pay.Constant.REDIS_NAMESPACE_PAY;
import static cn.quant.baa.pay.dict.MessageEnum.*;
/**
* Created by Administrator on 2021/8/24 0024.
*/
......@@ -20,22 +29,52 @@ public class TransactionController extends BusinessController {
@Autowired
private TransactionService transactionService;
@Autowired
private AcquirerPropertiesSource acquirerPropertiesSource;
@Autowired
private StringRedisTemplate stringRedisTemplate;
public AcquirerProperties check(ChannelRequestData data) {
if (data == null) {
AssertUtils.throwMessage(ILLEGAL_REQ_DATA);
}
String chanId = data.getChanId();
if (chanId == null) {
AssertUtils.throwMessage(ILLEGAL_REQ_CHAN_ID, chanId);
}
AcquirerProperties properties = acquirerPropertiesSource.get(Long.valueOf(chanId));
if (properties == null) {
AssertUtils.throwMessage(ACQUIRER_NOSUCH, chanId);
}
return properties;
}
@ResponseBody
@BusinessMapping(session = 1)
@PostMapping("/pay")
public ResponseEntity pay(@RequestBody BusinessRequest<PayRequestData> request) {
public ResponseEntity pay(@RequestBody BusinessRequest<PayRequestData> request) throws Exception {
PayRequestData requestData = request.getData();
ChannelResponse responseData = null;
try {
responseData = transactionService.pay(requestData);
} catch (Exception e) {
e.printStackTrace();
AcquirerProperties properties = check(requestData);
String institutionId = properties.getInstitutionId();
String productId = properties.getProductId();
String buyerId = requestData.getBuyerId();
String outTradeNo = requestData.getOutTradeNo();
String redisKey = StringUtils.toDelimitedString(REDIS_NAMESPACE_PAY, institutionId, productId, buyerId, outTradeNo);
String redisValue = stringRedisTemplate.opsForValue().get(redisKey);
if (redisValue != null) {
return ResponseEntity.ok(deserialize(redisValue));
}
ResponseEntity<HttpResponseData> responseEntity = succeed(responseData);
return responseEntity;
ChannelResponse channelResponse = transactionService.pay(properties, requestData);
HttpResponseData response = response(channelResponse);
if (channelResponse.getSuccess()) {
stringRedisTemplate.opsForValue().set(redisKey, serialize(response), PAY_DUE_TIME);
}
return ResponseEntity.ok(response);
}
@ResponseBody
......
package cn.quant.baa.pay.service;
import cn.quant.baa.pay.acquirer.AcquirerPropertiesSource;
import cn.quant.baa.pay.acquirer.AcquirerProperties;
import cn.quant.baa.pay.acquirer.ChannelResponse;
import cn.quant.baa.pay.acquirer.MerchantAcquirer;
import cn.quant.baa.pay.config.DictionaryViewer;
import cn.quant.baa.pay.context.TransactionSession;
import cn.quant.baa.pay.jpa.EntityBuilder;
import cn.quant.baa.pay.jpa.entity.*;
import cn.quant.baa.pay.model.web.*;
import cn.quant.baa.pay.util.AssertUtils;
import cn.quant.spring.util.StringUtils;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.ZonedDateTimeSerializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -17,10 +26,16 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.LinkedList;
import java.util.List;
import static cn.quant.baa.pay.dict.MessageEnum.*;
import static cn.quant.baa.pay.Constant.PAY_DUE_TIME;
import static cn.quant.baa.pay.dict.MessageEnum.ACQUIRER_NOMATCH;
import static cn.quant.baa.pay.dict.MessageEnum.EXIST_ORDER;
/**
* Created by Administrator on 2021/8/24 0024.
......@@ -30,15 +45,10 @@ public class TransactionService extends BusinessService {
private static final Logger logger = LoggerFactory.getLogger(TransactionService.class);
@Autowired
private AcquirerPropertiesSource acquirerPropertiesSource;
@Autowired
private MerchantAcquirer acquirer;
@Autowired
private DictionaryViewer dictionaryViewer;
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void check(PayHistoryIds ids, TransactionSession session) {
PayHistoryEntity historyEntity = session.findOne(PayHistoryEntity.class, ids);
......@@ -48,22 +58,18 @@ public class TransactionService extends BusinessService {
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public ChannelResponse pay(PayRequestData data) throws Exception {
public ChannelResponse pay(AcquirerProperties properties, PayRequestData data) throws Exception {
validate(data);
String chanId = data.getChanId();
AcquirerProperties profile = acquirerPropertiesSource.get(Long.valueOf(chanId));
if (profile == null) {
AssertUtils.throwMessage(ACQUIRER_NOSUCH, chanId);
}
String institutionId = profile.getInstitutionId();
String productId = profile.getProductId();
String buyerId = data.getBuyerId();
TransactionSession session = TransactionSession.session();
Long chanId = properties.getMchChanId();
String institutionId = properties.getInstitutionId();
String productId = properties.getProductId();
String buyerId = data.getBuyerId();
String outTradeNo = data.getOutTradeNo();
PayHistoryIds ids = new PayHistoryIds();
ids.setInstitutionId(institutionId);
ids.setProductId(productId);
......@@ -73,7 +79,7 @@ public class TransactionService extends BusinessService {
prepare(institutionId, productId, buyerId, session);
String mchId = data.getMchId();
if (!mchId.equals(profile.getPayAppId())) {
if (!mchId.equals(properties.getPayAppId())) {
AssertUtils.throwMessage(ACQUIRER_NOMATCH, chanId, mchId);
}
......@@ -90,24 +96,23 @@ public class TransactionService extends BusinessService {
details.add(detailEntity);
}
PayHistoryEntity history = EntityBuilder.payHistory(account, ids, historyId, data, profile, session);
long triggerId = session.nextId();
PayHistoryEntity history = EntityBuilder.payHistory(account, ids, historyId, data, properties, session);
ChannelResponse responseData = acquirer.pay(data, history);
if(responseData.getSuccess()){
if (responseData.getSuccess()) {
EntityBuilder.nextTxnNo(account, history);
BatchCycleTriggerEntity trigger = EntityBuilder.payTrigger(triggerId, history);
session.pushEntity(history);
session.pushEntity(trigger);
session.pushEntity(details);
history.setPayDueTime(LocalDateTime.now().plus(PAY_DUE_TIME));
history.setRequestId(responseData.getRequestId());
history.setDescText(responseData.getNotification());
session.commit();
session.addProperty(PayHistoryEntity.class, history);
session.addProperty(PayGoodsDetailEntity.class, details);
BatchCycleTriggerEntity trigger = EntityBuilder.payTrigger(session.nextId(), history);
trigger.setRequestId(session.getRequestId());
trigger.setNotifyUrl(data.getNotifyUrl());
session.pushEntity(history, trigger);
session.pushEntity(details);
session.commit();
}
return responseData;
......
app.id=baa-pay-server
apollo.meta=http://apollo-dev.quantgroups.com
\ No newline at end of file
......@@ -7,10 +7,10 @@ spring.devtools.restart.enabled=true
spring.devtools.livereload.enabled=true
#Server
server.port=8080
#server.port=8080
server.servlet.context-path=/
#quant
quant.server.number=1
#quant.server.number=1
quant.server.sequencer.operator=TWEPOCH_PLUS
quant.server.sequencer.sequence-bits=8
#Database
......
......@@ -21,4 +21,4 @@ spring:
cloud:
task:
initialize:
enable: false
\ No newline at end of file
enable: false
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true">
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>
<appender name="ROLLINGFILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logging.dir}/${project.name}/${project.name}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${logging.dir}/${project.name}/${project.name}.%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<maxHistory>90</maxHistory>
<maxFileSize>512MB</maxFileSize>
<totalSizeCap>10GB</totalSizeCap>
<cleanHistoryOnStart>true</cleanHistoryOnStart>
</rollingPolicy>
<encoder>
<charset>utf-8</charset>
<Pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}][%p][%t|%logger{1.}|%M] - %msg %ex{full}%n</Pattern>
</encoder>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!--<pattern>%-4relative [%thread] %-5level %logger{35} - %msg %ex{full}%n</pattern>-->
<pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}][%p][%t|%logger{1.}|%M] - %msg %ex{full}%n</pattern>
</encoder>
</appender>
<logger name="cn.quant.baa.pay" level="DEBUG"/>
<logger name="javax.activation" level="ERROR"/>
<logger name="org.quartz.core" level="ERROR"/>
<logger name="org.quartz.simpl" level="ERROR"/>
<logger name="io.lettuce.core" level="ERROR"/>
<logger name="io.netty" level="ERROR"/>
<logger name="com.zaxxer.hikari.pool" level="ERROR"/>
<logger name="com.alibaba.dubbo.common.extension" level="ERROR"/>
<logger name="sun.net.www.protocol.http" level="ERROR"/>
<logger name="com.ctrip.framework.apollo.internals" level="ERROR"/>
<logger name="org.apache.zookeeper" level="ERROR"/>
<logger name="org.apache.curator" level="ERROR"/>
<logger name="org.apache.coyote" level="ERROR"/>
<logger name="org.apache.catalina" level="ERROR"/>
<logger name="org.hibernate.hql" level="ERROR"/>
<logger name="org.hibernate.loader" level="ERROR"/>
<logger name="org.hibernate.type" level="ERROR"/>
<logger name="org.hibernate.persister" level="ERROR"/>
<logger name="org.hibernate.cfg" level="ERROR"/>
<logger name="org.hibernate.mapping" level="ERROR"/>
<logger name="org.hibernate.jpa.event" level="ERROR"/>
<logger name="org.hibernate.validator.internal" level="ERROR"/>
<logger name="org.hibernate.engine.internal" level="ERROR"/>
<logger name="org.hibernate.boot.model" level="ERROR"/>
<logger name="org.hibernate.boot.internal" level="ERROR"/>
<logger name="org.apache.tomcat.util" level="ERROR"/>
<logger name="org.springframework.orm" level="ERROR"/>
<logger name="org.springframework.jmx" level="ERROR"/>
<logger name="org.springframework.jndi" level="ERROR"/>
<logger name="org.springframework.aop" level="ERROR"/>
<logger name="org.springframework.web" level="ERROR"/>
<logger name="org.springframework.context" level="ERROR"/>
<logger name="org.springframework.core" level="ERROR"/>
<logger name="org.springframework.beans" level="ERROR"/>
<logger name="org.springframework.boot.web" level="ERROR"/>
<logger name="org.springframework.boot.actuate" level="ERROR"/>
<logger name="org.springframework.boot.context" level="ERROR"/>
<logger name="org.springframework.boot.autoconfigure.logging" level="ERROR"/>
<logger name="org.springframework.data.redis" level="ERROR"/>
<logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE"/>
<logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="ERROR"/>
<logger name="org.hibernate.engine.QueryParameters" level="ERROR"/>
<logger name="org.hibernate.SQL" level="ERROR" />
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ROLLINGFILE"/>
</root>
</configuration>
\ No newline at end of file
......@@ -7,9 +7,9 @@
</contextListener>
<appender name="ROLLINGFILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logging.dir}/${project.name}/${project.name}.log</file>
<file>/home/quant_group/logs/${project.name}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${logging.dir}/${project.name}/${project.name}.%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<fileNamePattern>/home/quant_group/logs/${project.name}.%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<maxHistory>90</maxHistory>
<maxFileSize>512MB</maxFileSize>
<totalSizeCap>10GB</totalSizeCap>
......@@ -67,7 +67,7 @@
<logger name="org.hibernate.engine.QueryParameters" level="ERROR"/>
<logger name="org.hibernate.SQL" level="ERROR" />
<root level="${logging.level}">
<root level="WARN">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ROLLINGFILE"/>
</root>
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment