针对邮乐的修改
This commit is contained in:
parent
a5e5ed2ecc
commit
8ae7a0fa53
|
@ -17,12 +17,13 @@ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certifi
|
||||||
COPY --from=builder /src /src
|
COPY --from=builder /src /src
|
||||||
|
|
||||||
RUN mkdir "/var/log/supervisor"
|
RUN mkdir "/var/log/supervisor"
|
||||||
|
RUN mkdir "/var/log/queue"
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
ADD ./sh/startup.sh /opt/startup.sh
|
ADD ./sh/startup.sh /opt/startup.sh
|
||||||
RUN sed -i 's/\r//g' /opt/startup.sh
|
RUN sed -i 's/\r//g' /opt/startup.sh
|
||||||
ADD ./sh/supervisord.conf /etc/supervisord.conf
|
ADD ./sh/supervisord.conf /etc/supervisord.conf
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
EXPOSE 10002
|
EXPOSE 10001
|
||||||
|
|
||||||
#CMD ["sh","/opt/startup.sh"]
|
#CMD ["sh","/opt/startup.sh"]
|
|
@ -10,6 +10,8 @@ type NacosConf struct {
|
||||||
NameSpace string
|
NameSpace string
|
||||||
TimeOut uint64
|
TimeOut uint64
|
||||||
ServiceName string
|
ServiceName string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
type RockerMqConfig struct {
|
type RockerMqConfig struct {
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
Name: transfer.rpc
|
|
||||||
ListenOn: 0.0.0.0:8080
|
|
||||||
Etcd:
|
|
||||||
Hosts:
|
|
||||||
- 127.0.0.1:2379
|
|
||||||
Key: transfer.rpc
|
|
|
@ -2,7 +2,6 @@ package do
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"trasfer_middleware/cmd/rpc/internal/logic/vo"
|
"trasfer_middleware/cmd/rpc/internal/logic/vo"
|
||||||
"trasfer_middleware/genModel"
|
"trasfer_middleware/genModel"
|
||||||
|
@ -13,17 +12,11 @@ func MarketKeyDataSet(order *genModel.ServerOrderMarket, resq string, resp strin
|
||||||
orderInfoReq map[string]interface{}
|
orderInfoReq map[string]interface{}
|
||||||
orderInfoRes map[string]interface{}
|
orderInfoRes map[string]interface{}
|
||||||
)
|
)
|
||||||
if orderInfoRes["code"].(string) != vo.ZLTX_RS_SUCCESS {
|
err = json.Unmarshal([]byte(resp), &orderInfoRes)
|
||||||
|
if orderInfoRes["errCode"].(string) != vo.MARKET_SUCCESS {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = json.Unmarshal([]byte(resq), &orderInfoReq)
|
err = json.Unmarshal([]byte(resq), &orderInfoReq)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("订单已存在")
|
|
||||||
}
|
|
||||||
err = json.Unmarshal([]byte(resp), &orderInfoRes)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -31,14 +24,14 @@ func MarketKeyDataSet(order *genModel.ServerOrderMarket, resq string, resp strin
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
order.ProductId, err = strconv.ParseInt(orderInfoReq["voucher_id"].(string), 16, 64)
|
order.ProductId = orderInfoReq["voucher_id"].(string)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
order.VoucherNum = orderInfoReq["mem_id"].(string)
|
order.VoucherNum = orderInfoReq["mem_id"].(string)
|
||||||
|
|
||||||
order.OutBizNo = orderInfoReq["req_serial_no"].(string)
|
order.OutBizNo = orderInfoReq["req_serial_no"].(string)
|
||||||
order.OrderNum = orderInfoReq["voucher_code"].(string)
|
order.OrderNum = orderInfoRes["data"].(map[string]interface{})["voucher_code"].(string)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
"trasfer_middleware/cmd/rpc/etc"
|
"trasfer_middleware/cmd/rpc/etc"
|
||||||
"trasfer_middleware/cmd/rpc/internal/logic/po"
|
"trasfer_middleware/cmd/rpc/internal/logic/po"
|
||||||
"trasfer_middleware/cmd/rpc/internal/logic/po/market/types"
|
"trasfer_middleware/cmd/rpc/internal/logic/po/market/types"
|
||||||
|
@ -33,14 +32,7 @@ func NewMarket(conf types.MarketConf) *Market {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Market) SetData(c context.Context, data map[string]interface{}, config *etc.RockerMqConfig) *MarketRequest {
|
func (r *Market) SetData(c context.Context, data map[string]interface{}, config *etc.RockerMqConfig) *MarketRequest {
|
||||||
|
p := "-----BEGIN RSA PRIVATE KEY-----\n" + data["sign"].(string) + "\n-----END RSA PRIVATE KEY-----"
|
||||||
data["timestamp"] = time.Now().Format("20060102150405")
|
|
||||||
private1 := "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC5FGH7Tq5u7pA/eh6AjAS0InykvWDJt095go8yK3w7+TRIhSYDdbRHlTgOQm4nWuMPfz3U2Rs1vJQwyyEYdylcYJ2zFLr7Vb1BdvkJ3Kz/2yJ6sz3BNq6xAHaeCKzA/WZxnc/ypfkGlrmfr2tNqCM9CUHUWryihBjLxwRiWLmo0aKgYpKLKYNixLgyqUYAifD3APncAduv6sSjUPMTyXMOlP1DXgVwX6IaUG/yV8/56Ew72Vdi/y4qZmCKMmXq4PovWrs8ISOEuhxbfLrGWbGCAVYPq7d7XaH+AOY4dhJZm7OZ43UGWw80QKGEPkvU4Oquzu8BqBh12md7Zsd6r0XzAgMBAAECggEAcLgTPKUc437z51UOwqeELdlbJFIaYn/8LTrwz1NgpH4P86L0FeNX2sjsjPK0d8+IvmV2WO2o/r9NWbI9A9N/Iz3MjcawYmZDj11QK0t1KZZil2wWzlfpaO+pTnJmFFvASq4ceeHPms2tW63QokkmvQOoTha9EBV3rJQW/XagDEolty57kkfmB31cQHJuAt+BF5EzBqv3q3jnqhsj8J/ddT0hadyKq65u85VomLH92asu/KKMKYYXC8aHjgX48chAmQUAHGM/HCD2owLHwtei2kPWNDx85ecBsglIX3wy0yhH1dnL+o3eeskVLl89ye3QCJPHJBaNUUfbgucgWT0bsQKBgQD1pPMAe31ZXajl9WlHMtn8qhpAGzi/GiiH6YrrHMQECC2GGuAakBko1Vhc+2HU35gwlPOhwMIOCapB0cCqcZVo3+71AKo78YvZLQ7yMuSsp0/Wn2N79NZ6+++wtHGPP9eHrLuWm23l15W7W0RcQptTaQupbculMQZ8b6cAjh6d1QKBgQDA4c4Xl2ePbQdgMMOuKTPPKF3QI1VhCVtxSV+Gj9MZBZedstz9+ZO3oxHhy8D5S9it1hE6dn6/a+7OWibZ/gBr1S0+11LcwKDb7q30dimr9bQs/srIywpoIIN8wVEkX4P9JLOWgQeAtq53IMba+cElef916aqyJpXuIek9lvUQpwKBgQCD7alNMwWpf3H8v4dhY+BLoRgkIfqiOGxYQogHqhVkjPfWNIzz9zxr/9lLZv+uEsBsJzOKRjpyy6ITY5H0eLhj8REnqMnFE/+mDlsenVLPn7Rzcns90ct3leOvpdnvs7wP9CdzxdqKPPUAAQ5/9o3xiFNpFbzv5Zq0LkslMy8iWQKBgQCiRJWctUxzllcRLpVBTPqAOkaKV195zmR2rzLFQvRmZZUDH7nZlQEYCgF+Q2tqj8uPm7tMwumo4wW55pAu7witr19sMbxNaWUrAeao9kvilkfpXsV9HYv4w/m6l+xKvGyPKDRJ1u1X9Nhb8mA5UsqSW8t2CIoJbHrQJwlRPlGXmwKBgQDg4rcsM2PmShOg8lSrHXPATXiZyyqpPJLpXbV6DRKyt7U6KWjyrplQN7yOoIUgsuD2OC/q67y7w1P3OY7X0RDnMr6MtIV0JyBJHg24eyBTqeLai2DqoHlsBOSvpJDZf+g/DXCjvHMWp1h0wqdj3aLthmU0dHM/CEqr/o7d8GwrGQ=="
|
|
||||||
p := "-----BEGIN RSA PRIVATE KEY-----\n" + private1 + "\n-----END RSA PRIVATE KEY-----"
|
|
||||||
data["app_id"] = "2783278"
|
|
||||||
data["mem_id"] = "2783278"
|
|
||||||
data["pos_id"] = "2783278"
|
|
||||||
|
|
||||||
sign, err := common.MarketMakeRsaSign(p, data)
|
sign, err := common.MarketMakeRsaSign(p, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -81,14 +73,7 @@ func (r *MarketRequest) request(url string) (*request.Response, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sysLog.LogSendMq(r.ctx, err)
|
sysLog.LogSendMq(r.ctx, err)
|
||||||
}
|
}
|
||||||
/*r.Model.Insert(context.Background(), &genModel.ServerMiddleMarketLogs{
|
return &resp, nil
|
||||||
Url : url,
|
|
||||||
Code :int64()
|
|
||||||
Data :string(reqStr)
|
|
||||||
Resp :resp.Text,
|
|
||||||
CreateTime :time.Now(),
|
|
||||||
})*/
|
|
||||||
return &resp, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *MarketRequest) KeySend() (*transfer.MarketKeySendRes, error) {
|
func (r *MarketRequest) KeySend() (*transfer.MarketKeySendRes, error) {
|
||||||
|
|
|
@ -11,4 +11,6 @@ const (
|
||||||
MARKET_KEY_QUERY = "openApi/v1/market/key/query"
|
MARKET_KEY_QUERY = "openApi/v1/market/key/query"
|
||||||
|
|
||||||
MARKET_LOG_STATU_DEFAULT = 1
|
MARKET_LOG_STATU_DEFAULT = 1
|
||||||
|
|
||||||
|
MARKET_SUCCESS = "00"
|
||||||
)
|
)
|
||||||
|
|
|
@ -181,6 +181,7 @@ func (m *Market) saveMarketOrder(logId int64, resq string, resp string) error {
|
||||||
order.LogId = logId
|
order.LogId = logId
|
||||||
order.ReqTime = time.Now()
|
order.ReqTime = time.Now()
|
||||||
order.CreateTime = time.Now()
|
order.CreateTime = time.Now()
|
||||||
|
order.Status = vo.MARKET_LOG_STATU_DEFAULT
|
||||||
err := do.MarketKeyDataSet(order, resq, resp)
|
err := do.MarketKeyDataSet(order, resq, resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -129,7 +129,7 @@ message MarketKeySendRes {
|
||||||
|
|
||||||
message MarketKeySendReq {
|
message MarketKeySendReq {
|
||||||
string app_id = 1;
|
string app_id = 1;
|
||||||
uint64 sign = 2;
|
string sign = 2;
|
||||||
string req_code = 3;
|
string req_code = 3;
|
||||||
string mem_id = 4;
|
string mem_id = 4;
|
||||||
string req_serial_no = 5;
|
string req_serial_no = 5;
|
||||||
|
|
|
@ -689,7 +689,7 @@ type MarketKeySendReq struct {
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
AppId string `protobuf:"bytes,1,opt,name=app_id,json=appId,proto3" json:"app_id,omitempty"`
|
AppId string `protobuf:"bytes,1,opt,name=app_id,json=appId,proto3" json:"app_id,omitempty"`
|
||||||
Sign uint64 `protobuf:"varint,2,opt,name=sign,proto3" json:"sign,omitempty"`
|
Sign string `protobuf:"bytes,2,opt,name=sign,proto3" json:"sign,omitempty"`
|
||||||
ReqCode string `protobuf:"bytes,3,opt,name=req_code,json=reqCode,proto3" json:"req_code,omitempty"`
|
ReqCode string `protobuf:"bytes,3,opt,name=req_code,json=reqCode,proto3" json:"req_code,omitempty"`
|
||||||
MemId string `protobuf:"bytes,4,opt,name=mem_id,json=memId,proto3" json:"mem_id,omitempty"`
|
MemId string `protobuf:"bytes,4,opt,name=mem_id,json=memId,proto3" json:"mem_id,omitempty"`
|
||||||
ReqSerialNo string `protobuf:"bytes,5,opt,name=req_serial_no,json=reqSerialNo,proto3" json:"req_serial_no,omitempty"`
|
ReqSerialNo string `protobuf:"bytes,5,opt,name=req_serial_no,json=reqSerialNo,proto3" json:"req_serial_no,omitempty"`
|
||||||
|
@ -740,11 +740,11 @@ func (x *MarketKeySendReq) GetAppId() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *MarketKeySendReq) GetSign() uint64 {
|
func (x *MarketKeySendReq) GetSign() string {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.Sign
|
return x.Sign
|
||||||
}
|
}
|
||||||
return 0
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *MarketKeySendReq) GetReqCode() string {
|
func (x *MarketKeySendReq) GetReqCode() string {
|
||||||
|
@ -2174,7 +2174,7 @@ var file_transfer_proto_rawDesc = []byte{
|
||||||
0x6b, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x12, 0x15, 0x0a,
|
0x6b, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x12, 0x15, 0x0a,
|
||||||
0x06, 0x61, 0x70, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61,
|
0x06, 0x61, 0x70, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61,
|
||||||
0x70, 0x70, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x67, 0x6e, 0x18, 0x02, 0x20, 0x01,
|
0x70, 0x70, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x67, 0x6e, 0x18, 0x02, 0x20, 0x01,
|
||||||
0x28, 0x04, 0x52, 0x04, 0x73, 0x69, 0x67, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x5f,
|
0x28, 0x09, 0x52, 0x04, 0x73, 0x69, 0x67, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x5f,
|
||||||
0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x72, 0x65, 0x71, 0x43,
|
0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x72, 0x65, 0x71, 0x43,
|
||||||
0x6f, 0x64, 0x65, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x65, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20,
|
0x6f, 0x64, 0x65, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x65, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20,
|
||||||
0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x65, 0x6d, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x72, 0x65,
|
0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x65, 0x6d, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x72, 0x65,
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
"trasfer_middleware/until/sysLog"
|
"trasfer_middleware/until/sysLog"
|
||||||
)
|
)
|
||||||
|
|
||||||
var configFile = flag.String("f", "../../../config/transfer.yaml", "the config file")
|
var configFile = flag.String("f", "../../../config/transfer_local.yaml", "the config file")
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
|
@ -18,7 +18,7 @@ import (
|
||||||
"trasfer_middleware/cmd/rpc/pb/transfer"
|
"trasfer_middleware/cmd/rpc/pb/transfer"
|
||||||
)
|
)
|
||||||
|
|
||||||
var configFile = flag.String("f", "../../config/transfer.yaml", "the config file")
|
var configFile = flag.String("f", "../../config/transfer_local.yaml", "the config file")
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
@ -51,6 +51,8 @@ func registerNacos(c *config.Config) {
|
||||||
NamespaceId: c.Nacos.NameSpace,
|
NamespaceId: c.Nacos.NameSpace,
|
||||||
TimeoutMs: c.Nacos.TimeOut,
|
TimeoutMs: c.Nacos.TimeOut,
|
||||||
NotLoadCacheAtStart: true,
|
NotLoadCacheAtStart: true,
|
||||||
|
Username: c.Nacos.Username,
|
||||||
|
Password: c.Nacos.Password,
|
||||||
LogDir: "/tmp/nacos/log",
|
LogDir: "/tmp/nacos/log",
|
||||||
CacheDir: "/tmp/nacos/cache",
|
CacheDir: "/tmp/nacos/cache",
|
||||||
LogLevel: "debug",
|
LogLevel: "debug",
|
||||||
|
|
|
@ -40,7 +40,7 @@ type (
|
||||||
OutBizNo string `db:"out_biz_no"` // 用户侧流水号
|
OutBizNo string `db:"out_biz_no"` // 用户侧流水号
|
||||||
VoucherNum string `db:"voucher_num"` // 商户号
|
VoucherNum string `db:"voucher_num"` // 商户号
|
||||||
OrderNum string `db:"order_num"` // 系统侧订单号
|
OrderNum string `db:"order_num"` // 系统侧订单号
|
||||||
ProductId int64 `db:"product_id"` // 平台提供商品id
|
ProductId string `db:"product_id"` // 平台提供商品id
|
||||||
Num int64 `db:"num"` // 购买数量
|
Num int64 `db:"num"` // 购买数量
|
||||||
LogId int64 `db:"log_id"` // 对应的日志id
|
LogId int64 `db:"log_id"` // 对应的日志id
|
||||||
ReqTime time.Time `db:"req_time"` // 请求时间
|
ReqTime time.Time `db:"req_time"` // 请求时间
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
{"@timestamp":"2024-07-12T18:11:31.120+08:00","caller":"market/market.go:96","content":"sendMq:[the topic=online_transfer_market route info not found]","level":"info","span":"5139dd7d49f98616","trace":"c1511bea8fd02ac0b3178bab68348d01"}
|
||||||
|
{"@timestamp":"2024-07-12T18:32:40.850+08:00","caller":"market/market.go:96","content":"sendMq:[producer group has been created]","level":"info","span":"49848e601ec056c7","trace":"010c0d971e83368fbfcb7c77bfd0c551"}
|
||||||
|
{"@timestamp":"2024-07-12T18:33:54.829+08:00","caller":"market/market.go:96","content":"sendMq:[the topic=online_transfer_market route info not found]","level":"info","span":"b3e8beb1de670367","trace":"ad085836fc7f1cd2ecf979b48ef9aea0"}
|
||||||
|
{"@timestamp":"2024-07-12T18:34:56.302+08:00","caller":"market/market.go:96","content":"sendMq:[the topic=testx_transfer_market route info not found]","level":"info","span":"1a5d63deb2ec9569","trace":"d59630d3ef3c72069d6e2a77990893d5"}
|
|
@ -0,0 +1 @@
|
||||||
|
{"@timestamp":"2024-07-15T11:34:59.386+08:00","caller":"market/market.go:87","content":"sendMq:[the topic=testx_transfer_market route info not found]","level":"info","span":"59dbedec4cddc60a","trace":"bfce456fdd339411405166a0fd601432"}
|
|
@ -7,8 +7,4 @@ V_REFLECT=""
|
||||||
|
|
||||||
|
|
||||||
docker build -t "${IMAGE}" . --no-cache
|
docker build -t "${IMAGE}" . --no-cache
|
||||||
docker stop "${RPC_CONTAINER}"
|
|
||||||
|
|
||||||
docker rm "${RPC_CONTAINER}"
|
|
||||||
|
|
||||||
docker run -it -p "${RPC_PORT}:${RPC_PORT}" --name "$RPC_CONTAINER" "${IMAGE}"
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
IMAGE="transfer_middleware"
|
||||||
|
RPC_CONTAINER="transfer_middleware"
|
||||||
|
RPC_PORT="10001"
|
||||||
|
V_REFLECT=""
|
||||||
|
TAGS_NAME="v1"
|
||||||
|
ADDRESS="registry.cn-chengdu.aliyuncs.com/go_ls/transfer_middleware_produce"
|
||||||
|
|
||||||
|
docker build -t "${IMAGE}:${TAGS_NAME}" . --no-cache
|
||||||
|
|
||||||
|
docker tag "${IMAGE}:${TAGS_NAME}" ${ADDRESS}:${TAGS_NAME}
|
||||||
|
|
||||||
|
docker push ${ADDRESS}:${TAGS_NAME}
|
|
@ -17,7 +17,8 @@ redirect_stderr=false
|
||||||
stdout_logfile_maxbytes = 20MB
|
stdout_logfile_maxbytes = 20MB
|
||||||
#stdout 日志文件备份数
|
#stdout 日志文件备份数
|
||||||
stdout_logfile_backups = 20
|
stdout_logfile_backups = 20
|
||||||
|
stdout_logfile=/var/log/out.log
|
||||||
|
stderr_logfile=/var/log/err.log
|
||||||
|
|
||||||
[program:queue]
|
[program:queue]
|
||||||
directory=/src/cmd/rpc/queue
|
directory=/src/cmd/rpc/queue
|
||||||
|
@ -37,3 +38,5 @@ redirect_stderr=false
|
||||||
stdout_logfile_maxbytes = 20MB
|
stdout_logfile_maxbytes = 20MB
|
||||||
#stdout 日志文件备份数
|
#stdout 日志文件备份数
|
||||||
stdout_logfile_backups = 20
|
stdout_logfile_backups = 20
|
||||||
|
stdout_logfile=/var/log/queue/out.log
|
||||||
|
stderr_logfile=/var/log/queue/err.log
|
156
test/encrypt.go
156
test/encrypt.go
|
@ -1,156 +0,0 @@
|
||||||
package test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/aes"
|
|
||||||
"crypto/cipher"
|
|
||||||
crand "crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"io"
|
|
||||||
"math/rand"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
const lettersString = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
||||||
|
|
||||||
// 字符串长度
|
|
||||||
const number = 16
|
|
||||||
|
|
||||||
/*
|
|
||||||
16位码,前15位随机字符串,最后一位通过前15位字符串计算校验生成
|
|
||||||
*/
|
|
||||||
|
|
||||||
func LotteryEncryptEncode() string {
|
|
||||||
b := make([]byte, number)
|
|
||||||
var sum byte
|
|
||||||
for i := 0; i < number-1; i++ {
|
|
||||||
b[i] = lettersString[rand.Int63()%int64(len(lettersString))]
|
|
||||||
sum += b[i]
|
|
||||||
}
|
|
||||||
b[number-1] = lettersString[sum%byte(len(lettersString))]
|
|
||||||
return *(*string)(unsafe.Pointer(&b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func LotteryEncryptDecode(str string) bool {
|
|
||||||
if len(str) != number {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
var sum byte
|
|
||||||
for i := 0; i < len(str)-1; i++ {
|
|
||||||
sum += str[i]
|
|
||||||
}
|
|
||||||
if lettersString[sum%byte(len(lettersString))] != str[len(str)-1] {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// =================== CBC ======================
|
|
||||||
func AesEncryptCBC(origData []byte, key []byte) (str string) {
|
|
||||||
// 分组秘钥
|
|
||||||
// NewCipher该函数限制了输入k的长度必须为16, 24或者32
|
|
||||||
block, _ := aes.NewCipher(key)
|
|
||||||
blockSize := block.BlockSize() // 获取秘钥块的长度
|
|
||||||
origData = pkcs5Padding(origData, blockSize) // 补全码
|
|
||||||
blockMode := cipher.NewCBCEncrypter(block, key[:blockSize]) // 加密模式
|
|
||||||
encrypted := make([]byte, len(origData)) // 创建数组
|
|
||||||
blockMode.CryptBlocks(encrypted, origData) // 加密
|
|
||||||
|
|
||||||
return base64.StdEncoding.EncodeToString(encrypted)
|
|
||||||
}
|
|
||||||
func AesDecryptCBC(data string, key []byte) (decrypted []byte) {
|
|
||||||
encrypted, err := base64.StdEncoding.DecodeString(data)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
block, _ := aes.NewCipher(key) // 分组秘钥
|
|
||||||
blockSize := block.BlockSize() // 获取秘钥块的长度
|
|
||||||
blockMode := cipher.NewCBCDecrypter(block, key[:blockSize]) // 加密模式
|
|
||||||
decrypted = make([]byte, len(encrypted)) // 创建数组
|
|
||||||
blockMode.CryptBlocks(decrypted, encrypted) // 解密
|
|
||||||
decrypted = pkcs5UnPadding(decrypted) // 去除补全码
|
|
||||||
return decrypted
|
|
||||||
}
|
|
||||||
func pkcs5Padding(ciphertext []byte, blockSize int) []byte {
|
|
||||||
padding := blockSize - len(ciphertext)%blockSize
|
|
||||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
|
||||||
return append(ciphertext, padtext...)
|
|
||||||
}
|
|
||||||
func pkcs5UnPadding(origData []byte) []byte {
|
|
||||||
length := len(origData)
|
|
||||||
unpadding := int(origData[length-1])
|
|
||||||
return origData[:(length - unpadding)]
|
|
||||||
}
|
|
||||||
|
|
||||||
// =================== ECB ======================
|
|
||||||
func AesEncryptECB(origData []byte, key []byte) (encrypted []byte) {
|
|
||||||
cipher, _ := aes.NewCipher(generateKey(key))
|
|
||||||
length := (len(origData) + aes.BlockSize) / aes.BlockSize
|
|
||||||
plain := make([]byte, length*aes.BlockSize)
|
|
||||||
copy(plain, origData)
|
|
||||||
pad := byte(len(plain) - len(origData))
|
|
||||||
for i := len(origData); i < len(plain); i++ {
|
|
||||||
plain[i] = pad
|
|
||||||
}
|
|
||||||
encrypted = make([]byte, len(plain))
|
|
||||||
// 分组分块加密
|
|
||||||
for bs, be := 0, cipher.BlockSize(); bs <= len(origData); bs, be = bs+cipher.BlockSize(), be+cipher.BlockSize() {
|
|
||||||
cipher.Encrypt(encrypted[bs:be], plain[bs:be])
|
|
||||||
}
|
|
||||||
|
|
||||||
return encrypted
|
|
||||||
}
|
|
||||||
func AesDecryptECB(encrypted []byte, key []byte) (decrypted []byte) {
|
|
||||||
cipher, _ := aes.NewCipher(generateKey(key))
|
|
||||||
decrypted = make([]byte, len(encrypted))
|
|
||||||
//
|
|
||||||
for bs, be := 0, cipher.BlockSize(); bs < len(encrypted); bs, be = bs+cipher.BlockSize(), be+cipher.BlockSize() {
|
|
||||||
cipher.Decrypt(decrypted[bs:be], encrypted[bs:be])
|
|
||||||
}
|
|
||||||
|
|
||||||
trim := 0
|
|
||||||
if len(decrypted) > 0 {
|
|
||||||
trim = len(decrypted) - int(decrypted[len(decrypted)-1])
|
|
||||||
}
|
|
||||||
|
|
||||||
return decrypted[:trim]
|
|
||||||
}
|
|
||||||
func generateKey(key []byte) (genKey []byte) {
|
|
||||||
genKey = make([]byte, 16)
|
|
||||||
copy(genKey, key)
|
|
||||||
for i := 16; i < len(key); {
|
|
||||||
for j := 0; j < 16 && i < len(key); j, i = j+1, i+1 {
|
|
||||||
genKey[j] ^= key[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return genKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// =================== CFB ======================
|
|
||||||
func AesEncryptCFB(origData []byte, key []byte) (encrypted []byte) {
|
|
||||||
block, err := aes.NewCipher(key)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
encrypted = make([]byte, aes.BlockSize+len(origData))
|
|
||||||
iv := encrypted[:aes.BlockSize]
|
|
||||||
if _, err := io.ReadFull(crand.Reader, iv); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
stream := cipher.NewCFBEncrypter(block, iv)
|
|
||||||
stream.XORKeyStream(encrypted[aes.BlockSize:], origData)
|
|
||||||
return encrypted
|
|
||||||
}
|
|
||||||
func AesDecryptCFB(encrypted []byte, key []byte) (decrypted []byte) {
|
|
||||||
block, _ := aes.NewCipher(key)
|
|
||||||
if len(encrypted) < aes.BlockSize {
|
|
||||||
panic("ciphertext too short")
|
|
||||||
}
|
|
||||||
iv := encrypted[:aes.BlockSize]
|
|
||||||
encrypted = encrypted[aes.BlockSize:]
|
|
||||||
|
|
||||||
stream := cipher.NewCFBDecrypter(block, iv)
|
|
||||||
stream.XORKeyStream(encrypted, encrypted)
|
|
||||||
return encrypted
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
package market
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"qteam/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MarketClient struct {
|
|
||||||
cfg config.MarketConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
type MarketSendRequest struct {
|
|
||||||
AppId string `json:"app_id"` //APP ID
|
|
||||||
Sign string `json:"sign"` //签名
|
|
||||||
ReqCode string `json:"req_code"` //固定值:voucher.create
|
|
||||||
MemId string `json:"mem_id"` //商户号
|
|
||||||
ReqSerialNo string `json:"req_serial_no"` //请求唯一流水号 最大32位
|
|
||||||
TimeTamp string `json:"timestamp"` //时间戳 yyyyMMddHHmmss
|
|
||||||
PosId string `json:"pos_id"` //商户方平台号
|
|
||||||
VoucherId string `json:"voucher_id"` //制码批次号
|
|
||||||
VoucherNum int `json:"voucher_num"` //请券数量,默认是 1
|
|
||||||
MobileNo string `json:"mobile_no"` //11 手机号,可传空字符串
|
|
||||||
SendMsg string `json:"send_msg"` //是否发送短信:2- 发送 1-不发送
|
|
||||||
}
|
|
||||||
|
|
||||||
type MarketSenResponse struct {
|
|
||||||
VoucherId string `json:"voucher_id"` //制码批次号
|
|
||||||
VoucherCode string `json:"voucher_code"` //券码
|
|
||||||
ShortUrl string `json:"short_url"` //含二维码、条码的短链接
|
|
||||||
VoucherSdate string `json:"voucher_sdate"` //有效期起
|
|
||||||
VoucherEdate string `json:"voucher_edate"` //有效期止
|
|
||||||
CodeType string `json:"code_type"` //码类型: 00- 代金券 01- 满减券
|
|
||||||
}
|
|
||||||
|
|
||||||
type MarketResponse struct {
|
|
||||||
ErrCode string `json:"errCode"` //00-成功 其他:失败
|
|
||||||
Msg string `json:"msg"` //描 述 (失败时必填)
|
|
||||||
Data MarketSenResponse `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (this *MarketSendRequest) toMap() (resultMap map[string]interface{}) {
|
|
||||||
// Marshal the struct to JSON, ignoring omitempty fields.
|
|
||||||
jsonBytes, err := json.Marshal(this)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Unmarshal the JSON into a map to get the final result.
|
|
||||||
err = json.Unmarshal(jsonBytes, &resultMap)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return resultMap
|
|
||||||
}
|
|
||||||
|
|
||||||
func (this *MarketClient) doPost(url string, jsonBytes []byte) (body []byte, err error) {
|
|
||||||
// 创建POST请求
|
|
||||||
url = this.cfg.Host + url
|
|
||||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBytes))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置Content-Type头
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
// 创建HTTP客户端
|
|
||||||
client := &http.Client{}
|
|
||||||
|
|
||||||
// 发送请求并处理响应
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// 读取响应体
|
|
||||||
body, err = ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
package market
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"qteam/app/utils/encrypt"
|
|
||||||
"qteam/config"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewMarketClient(cfg config.MarketConfig) *MarketClient {
|
|
||||||
cfg.Sign = "-----BEGIN RSA PRIVATE KEY-----\n" + cfg.Sign + "\n-----END RSA PRIVATE KEY-----"
|
|
||||||
return &MarketClient{
|
|
||||||
cfg: cfg,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
MarketSend
|
|
||||||
券码生成接口
|
|
||||||
- 请求地址:/openApi/v1/market/key/send
|
|
||||||
- 说明:发券接口应支持使用同一流水号进行重复请求,即:当调用该接口失败时,可 以使用同一流水号进行再次请求,接口需要根据请求的流水号进行判断,若无该流水 号的券码信息则新生成后返回,若有该流水号的券码信息则直接返回该券码的信息
|
|
||||||
orderNo: 订单号
|
|
||||||
VoucherId: 制码批次号
|
|
||||||
MobileNo: 11 手机号,可传空字符串
|
|
||||||
SendMsg: 是否发送短信:2- 发送 1-不发送
|
|
||||||
*/
|
|
||||||
func (this *MarketClient) MarketSend(orderNo, VoucherId, MobileNo, SendMsg string) (res MarketResponse, err error) {
|
|
||||||
url := "/openApi/v1/market/key/send"
|
|
||||||
request := MarketSendRequest{
|
|
||||||
AppId: this.cfg.AppId,
|
|
||||||
ReqCode: this.cfg.ReqCode,
|
|
||||||
MemId: this.cfg.MemId,
|
|
||||||
PosId: this.cfg.PosId,
|
|
||||||
TimeTamp: time.Now().Format("20060102150405"),
|
|
||||||
VoucherId: VoucherId,
|
|
||||||
ReqSerialNo: orderNo,
|
|
||||||
VoucherNum: 1,
|
|
||||||
MobileNo: MobileNo,
|
|
||||||
SendMsg: SendMsg,
|
|
||||||
}
|
|
||||||
|
|
||||||
request.Sign, err = MakeRsaSign(this.cfg.Sign, request.toMap())
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
bytes, err := json.Marshal(request)
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := this.doPost(url, bytes)
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(data, &res)
|
|
||||||
// 加密
|
|
||||||
res.Data.ShortUrl = encrypt.AesEncryptCBC([]byte(res.Data.ShortUrl), []byte(this.cfg.SecretKey))
|
|
||||||
return res, err
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
package market
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/qit-team/snow-core/kernel/server"
|
|
||||||
"os"
|
|
||||||
"qteam/app/utils"
|
|
||||||
"qteam/config"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMarketSendRequest_Market(t *testing.T) {
|
|
||||||
opts := config.GetOptions()
|
|
||||||
if opts.ShowVersion {
|
|
||||||
fmt.Printf("%s\ncommit %s\nbuilt on %s\n", server.Version, server.BuildCommit, server.BuildDate)
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
//加载配置
|
|
||||||
conf, err := config.Load(opts.ConfFile)
|
|
||||||
if err != nil {
|
|
||||||
utils.Log(nil, "err", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
client := NewMarketClient(conf.OpenApiMarketConfig)
|
|
||||||
|
|
||||||
data, err := client.MarketSend("123456789111", "1717567048171", "", "2")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
t.Log(data)
|
|
||||||
}
|
|
|
@ -1,137 +0,0 @@
|
||||||
package market
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/sha256"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/pem"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
)
|
|
||||||
|
|
||||||
// getSignString 使用 xx=aa&yy=bb 的字符串拼接
|
|
||||||
func getSignString(data map[string]interface{}) string {
|
|
||||||
keys := make([]string, 0, len(data))
|
|
||||||
for key := range data {
|
|
||||||
keys = append(keys, key)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
|
|
||||||
signString := ""
|
|
||||||
separator := ""
|
|
||||||
for _, key := range keys {
|
|
||||||
value := data[key]
|
|
||||||
if key == "sign" || value == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
signString += fmt.Sprintf("%s%s=%v", separator, key, value)
|
|
||||||
separator = "&"
|
|
||||||
}
|
|
||||||
return signString
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyRsaSign 签名验证
|
|
||||||
func VerifyRsaSign(publicKey string, data map[string]interface{}) (map[string]interface{}, error) {
|
|
||||||
// 对 sign nonce timestamp appId 升序排序
|
|
||||||
// 使用 xx=aa&yy=bb 的字符串拼接
|
|
||||||
// 商户的公钥验签 RSA2验签
|
|
||||||
signString := getSignString(data)
|
|
||||||
|
|
||||||
rsaPubKey, err := parseRSAPublicKeyFromPEM([]byte(publicKey))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
signature, err := base64.StdEncoding.DecodeString(data["sign"].(string))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
hashed := sha256.Sum256([]byte(signString))
|
|
||||||
err = rsa.VerifyPKCS1v15(rsaPubKey, crypto.SHA256, hashed[:], signature)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("签名验证失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeRsaSign 生成签名
|
|
||||||
func MakeRsaSign(privateKey string, data map[string]interface{}) (string, error) {
|
|
||||||
// 对 sign nonce timestamp appId 升序排序
|
|
||||||
// 使用 xx=aa&yy=bb 的字符串拼接
|
|
||||||
// 营销系统生成的私钥生成签名 RSA2加签
|
|
||||||
signString := getSignString(data)
|
|
||||||
|
|
||||||
privKey, err := parseRSAPrivateKeyFromPEM([]byte(privateKey))
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.New("私钥解析失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
hashed := sha256.Sum256([]byte(signString))
|
|
||||||
signature, err := rsa.SignPKCS1v15(rand.Reader, privKey, crypto.SHA256, hashed[:])
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.New("签名失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
return base64.StdEncoding.EncodeToString(signature), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseRSAPrivateKeyFromPEM 解析私钥
|
|
||||||
func parseRSAPrivateKeyFromPEM(key []byte) (*rsa.PrivateKey, error) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// Parse PEM block
|
|
||||||
var block *pem.Block
|
|
||||||
if block, _ = pem.Decode(key); block == nil {
|
|
||||||
return nil, errors.New("私钥解析失败: 无效的PEM格式")
|
|
||||||
}
|
|
||||||
|
|
||||||
var parsedKey interface{}
|
|
||||||
if parsedKey, err = x509.ParsePKCS1PrivateKey(block.Bytes); err != nil {
|
|
||||||
if parsedKey, err = x509.ParsePKCS8PrivateKey(block.Bytes); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var pkey *rsa.PrivateKey
|
|
||||||
var ok bool
|
|
||||||
if pkey, ok = parsedKey.(*rsa.PrivateKey); !ok {
|
|
||||||
return nil, errors.New("密钥不是有效的RSA私钥")
|
|
||||||
}
|
|
||||||
|
|
||||||
return pkey, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseRSAPublicKeyFromPEM parses a PEM encoded PKCS1 or PKCS8 public key
|
|
||||||
func parseRSAPublicKeyFromPEM(key []byte) (*rsa.PublicKey, error) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// Parse PEM block
|
|
||||||
var block *pem.Block
|
|
||||||
if block, _ = pem.Decode(key); block == nil {
|
|
||||||
return nil, errors.New("公钥解析失败: 无效的PEM格式")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the key
|
|
||||||
var parsedKey interface{}
|
|
||||||
if parsedKey, err = x509.ParsePKIXPublicKey(block.Bytes); err != nil {
|
|
||||||
if cert, err := x509.ParseCertificate(block.Bytes); err == nil {
|
|
||||||
parsedKey = cert.PublicKey
|
|
||||||
} else {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var pkey *rsa.PublicKey
|
|
||||||
var ok bool
|
|
||||||
if pkey, ok = parsedKey.(*rsa.PublicKey); !ok {
|
|
||||||
return nil, errors.New("密钥不是有效的RSA公钥")
|
|
||||||
}
|
|
||||||
|
|
||||||
return pkey, nil
|
|
||||||
}
|
|
Loading…
Reference in New Issue