diff --git a/internal/biz/alarm.go b/internal/biz/alarm.go new file mode 100644 index 0000000..09945e2 --- /dev/null +++ b/internal/biz/alarm.go @@ -0,0 +1,69 @@ +package biz + +import ( + "context" + "fmt" + "github.com/redis/go-redis/v9" + "voucher/internal/biz/bo" + "voucher/internal/biz/vo" + "voucher/internal/pkg/lock" +) + +func (v *VoucherBiz) alarm(ctx context.Context, order *bo.OrderBo, errMsg string) error { + + // 1小时 内 指定的批次号 发放 发生错误 预警 + c := vo.OrderConsumeFailAlarmKey.BuildCache([]string{order.ProductNo}) + + _, err := v.rdb.Rdb.Get(ctx, c.Key).Result() + + if err == nil { + // 缓存存在,直接返回 + return nil + } + + if err != redis.Nil { + return fmt.Errorf(fmt.Sprintf("alarm 获取redis缓存%s异常:%v", c.Key, err)) + } + + cl := vo.OrderConsumeFailAlarmLockKey.BuildCache([]string{order.ProductNo}) + + return lock.NewMutex(v.rdb.Rdb, cl.TTL).Lock(ctx, cl.Key, func(ctx context.Context) error { + // 二次获取,判定处理,以免获取锁后又执行了一次 + + cacheValue, err3 := v.rdb.Rdb.Get(ctx, c.Key).Result() + + if err3 != nil && err3 != redis.Nil { + return fmt.Errorf(fmt.Sprintf("alarm 二次获取redis缓存%s异常:%v", c.Key, err)) + } + + if len(cacheValue) > 0 { + return nil // 有直接返回 + } + + // 通知 + if err = v.DingMixRepo.SendMarkdownMessage(ctx, "异常通知", v.alarmText(ctx, order, errMsg)); err != nil { + return err + } + + if err = v.rdb.Rdb.Set(ctx, c.Key, order.ProductNo, c.TTL).Err(); err != nil { + return fmt.Errorf(fmt.Sprintf("设置redis缓存%s异常:%v", c.Key, err)) + } + + return nil + }) +} + +func (v *VoucherBiz) alarmText(_ context.Context, order *bo.OrderBo, errMsg string) string { + + remarks := fmt.Sprintf("订单号:%s,商品编号:%s,原因:%s", order.OrderNo, order.ProductNo, errMsg) + + msg := "# " + + "

立减金发放平台报警通知

" + + "
\n" + + "" + + "不好了,订单发放发生异常了" + + "[%s]请尽快处理@相关人员。" + + "" + + return fmt.Sprintf(msg, remarks) +} diff --git a/internal/biz/cmb.go b/internal/biz/cmb.go index c394d85..26ded5e 100644 --- a/internal/biz/cmb.go +++ b/internal/biz/cmb.go @@ -1,162 +1 @@ package biz - -import ( - "context" - "fmt" - "time" - err2 "voucher/api/err" - v1 "voucher/api/v1" - "voucher/internal/biz/bo" - "voucher/internal/biz/vo" - "voucher/internal/pkg/lock" -) - -func (v *VoucherBiz) GetByOutBizNo(ctx context.Context, req *bo.OrderCreateReqBo) (*bo.OrderBo, error) { - - order, err := v.OrderRepo.GetByOutBizNo(ctx, vo.OrderTypeCmb, req.OutBizNo) - - if err != nil && !err2.IsDbNotFound(err) { - return nil, err - } - - return order, nil -} - -func (v *VoucherBiz) CmbOrder(ctx context.Context, req *bo.OrderCreateReqBo) (orderNo string, err error) { - - order, err3 := v.GetByOutBizNo(ctx, req) - if err3 != nil { - return orderNo, err3 - } - - if order != nil { - - if order.Status.IsFail() { - - if err4 := v.orderRetry(ctx, order); err4 != nil { - return orderNo, err4 - } - } - - orderNo = order.OrderNo - return orderNo, err - } - - product, err3 := v.ProductRepo.GetByProductNo(ctx, req.ProductNo) - if err3 != nil { - return orderNo, err3 - } - - order, err3 = v.order(ctx, req, product) - if err3 != nil { - return orderNo, err3 - } - - orderNo = order.OrderNo - - return orderNo, nil -} - -func (v *VoucherBiz) CmbQuery(ctx context.Context, orderNo string) (resp *v1.CmbQueryReply, err error) { - - c := vo.CmbQueryLockKey.BuildCache([]string{orderNo}) - - err = lock.NewMutex(v.rdb.Rdb, c.TTL).Lock(ctx, c.Key, func(ctx context.Context) error { - - order, err3 := v.OrderRepo.GetByOrderNo(ctx, orderNo) - if err3 != nil { - return err3 - } - - if err = v.Query(ctx, order); err != nil { - return err - } - - status, err3 := order.Status.GetCmbStatusText() - if err3 != nil { - return err3 - } - - resp = &v1.CmbQueryReply{ - Ticket: order.OrderNo, - Status: status.GetValue(), - TransDate: time.Now().Format("20060102150405"), - OrgNo: v.bc.Cmb.OrgNo, - Ext: "", - } - - return nil - }) - - return -} - -func (v *VoucherBiz) CmbProductQuery(ctx context.Context, productNo string) (reps *v1.CmbQueryProductReply, err error) { - - c := vo.CmbProductQueryLockKey.BuildCache([]string{productNo}) - - err = lock.NewMutex(v.rdb.Rdb, c.TTL).Lock(ctx, c.Key, func(ctx context.Context) error { - - product, err3 := v.ProductRepo.GetByProductNo(ctx, productNo) - if err3 != nil { - return err3 - } - - if !product.Channel.IsWeChat() { - return fmt.Errorf("只支持微信") - } - - wechatResp, err4 := v.WechatCpnRepo.QueryProduct(ctx, product.MchId, product.BatchNo) - if err4 != nil { - return err4 - } - - reps = &v1.CmbQueryProductReply{ - RespCode: vo.CmbResponseStatusSuccess.GetValue(), - RespMsg: "成功", - ActivityName: product.Name, - ActivityId: product.ProductNo, - Amount: "", - MinAmount: "", - AvailableType: "", - AvailableDays: "", // 动态有效期天数 - StartTime: "", - EndTime: "", - AvailableStock: "", - Detail: *wechatResp.Description, - } - - inputFormat := time.RFC3339 - - if wechatResp.AvailableBeginTime != nil { - - availableBeginTime, _ := time.Parse(inputFormat, *wechatResp.AvailableBeginTime) - reps.StartTime = availableBeginTime.Format("2006-01-02 15:04:05.000") - reps.SaleStartTime = reps.StartTime - } - - if wechatResp.AvailableEndTime != nil { - availableEndTime, _ := time.Parse(inputFormat, *wechatResp.AvailableEndTime) - reps.EndTime = availableEndTime.Format("2006-01-02 15:04:05.000") - reps.SaleEndTime = reps.EndTime - } - - reps.Amount = fmt.Sprintf("%d", *wechatResp.StockUseRule.FixedNormalCoupon.CouponAmount) - reps.MinAmount = fmt.Sprintf("%d", *wechatResp.StockUseRule.FixedNormalCoupon.TransactionMinimum) - - availableStock := *wechatResp.StockUseRule.MaxCoupons - *wechatResp.DistributedCoupons - reps.AvailableStock = fmt.Sprintf("%d", availableStock) - - availableType, err3 := product.AvailableType.GetCmbAvailableType() - if err3 != nil { - return err3 - } - - reps.AvailableType = availableType.GetValue() - reps.AvailableDays = fmt.Sprintf("%d", product.AvailableDays) - - return nil - }) - - return -} diff --git a/internal/biz/kx/bb_to_wechat.go b/internal/biz/kx/bb_to_wechat.go new file mode 100644 index 0000000..49fb478 --- /dev/null +++ b/internal/biz/kx/bb_to_wechat.go @@ -0,0 +1,33 @@ +package kx + +// BBToWechatRequest 蓝色兄弟请求微信发券接口数据同步Api +type BBToWechatRequest struct { + // 微信为每个批次分配的唯一id + StockId string `protobuf:"bytes,9,opt,name=stockId,proto3" json:"stockId,omitempty"` + // 商户此次发放凭据号(格式:商户id+日期+流水号) + OutRequestNo string `protobuf:"bytes,10,opt,name=outRequestNo,proto3" json:"outRequestNo,omitempty"` + // 微信为发券方商户分配的公众账号ID + AppId string `protobuf:"bytes,11,opt,name=appId,proto3" json:"appId,omitempty"` + // 批次创建方商户号 + StockCreatorMhId string `protobuf:"bytes,12,opt,name=stockCreatorMhId,json=stockCreatorMchid,proto3" json:"stockCreatorMhId,omitempty"` + // 券面额,单位:分 + CouponValue int32 `protobuf:"bytes,13,opt,name=couponValue,proto3" json:"couponValue,omitempty"` + // 面额发券批次门槛,单位:分 + CouponMinimum int32 `protobuf:"bytes,14,opt,name=couponMinimum,proto3" json:"couponMinimum,omitempty"` + // 微信为代金券唯一分配的id, 在微信请求失败时可能为空 + CouponId string `protobuf:"bytes,15,opt,name=couponId,proto3" json:"couponId,omitempty"` + // 微信返回结果 + WxRes string `protobuf:"bytes,16,opt,name=wxRes,proto3" json:"wxRes,omitempty"` + // 招行返回结果 + CmbRes string `protobuf:"bytes,17,opt,name=cmbRes,proto3" json:"cmbRes,omitempty"` + // 招行此次请求的数据的唯一流水号 + TransactionId string `protobuf:"bytes,18,opt,name=transactionId,proto3" json:"transactionId,omitempty"` +} + +func (this *BBToWechatRequest) GetSynNotice() *SynNotice { + return &SynNotice{ + OutBizBo: this.TransactionId, + Type: SynNoticeTypeBBToWechat, + BizContent: this, + } +} diff --git a/internal/biz/kx/cmb_to_bb.go b/internal/biz/kx/cmb_to_bb.go new file mode 100644 index 0000000..061db53 --- /dev/null +++ b/internal/biz/kx/cmb_to_bb.go @@ -0,0 +1,27 @@ +package kx + +// CmbToBBRequest 招行请求蓝色兄弟发券接口数据同步Api +type CmbToBBRequest struct { + // 唯一流水号 + TransactionId string `protobuf:"bytes,9,opt,name=transactionId,proto3" json:"transactionId,omitempty"` + // 外部合作方权益批次号 + ActivityId string `protobuf:"bytes,10,opt,name=activityId,proto3" json:"activityId,omitempty"` + // 招商银行用户号 用户标识,比如手机号、支付宝openId + CmbUid string `protobuf:"bytes,11,opt,name=cmbUid,proto3" json:"cmbUid,omitempty"` + // 用户标识类型,0-手机号,1-支付宝openId + CmbUidType string `protobuf:"bytes,12,opt,name=cmbUidType,proto3" json:"cmbUidType,omitempty"` + // 时间戳,长度为13位,精度为毫秒 + Timestamp string `protobuf:"bytes,13,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + // appId + AppId string `protobuf:"bytes,14,opt,name=appId,proto3" json:"appId,omitempty"` + // 补丁 + Attach string `protobuf:"bytes,15,opt,name=attach,proto3" json:"attach,omitempty"` +} + +func (this *CmbToBBRequest) GetSynNotice() *SynNotice { + return &SynNotice{ + OutBizBo: this.TransactionId, + Type: SynNoticeTypeCmbToBB, + BizContent: this, + } +} diff --git a/internal/biz/kx/kx.go b/internal/biz/kx/kx.go new file mode 100644 index 0000000..6f73a5c --- /dev/null +++ b/internal/biz/kx/kx.go @@ -0,0 +1,38 @@ +package kx + +type SynNoticeType uint8 + +const ( + SynNoticeTypeCmbToBB SynNoticeType = iota + 1 + SynNoticeTypeBBToWechat + SynNoticeTypeWechatToBB +) + +var SynNoticeTypeMap = map[SynNoticeType]string{ + SynNoticeTypeCmbToBB: "招行请求蓝色兄弟", + SynNoticeTypeBBToWechat: "蓝色兄弟请求微信", + SynNoticeTypeWechatToBB: "微信请求蓝色兄弟", +} + +func (s SynNoticeType) GetText() string { + if t, ok := SynNoticeTypeMap[s]; ok { + return t + } + return "未知类型" +} + +func (s SynNoticeType) GetValue() uint8 { + return uint8(s) +} + +func (s SynNoticeType) IsCmbToBB() bool { + return s == SynNoticeTypeCmbToBB +} + +func (s SynNoticeType) IsBBToWechat() bool { + return s == SynNoticeTypeBBToWechat +} + +func (s SynNoticeType) IsWechatToBB() bool { + return s == SynNoticeTypeWechatToBB +} diff --git a/internal/biz/kx/kx_notice.go b/internal/biz/kx/kx_notice.go new file mode 100644 index 0000000..f64c644 --- /dev/null +++ b/internal/biz/kx/kx_notice.go @@ -0,0 +1,23 @@ +package kx + +import ( + "encoding/json" +) + +var _ SynApiInterface = (*CmbToBBRequest)(nil) +var _ SynApiInterface = (*BBToWechatRequest)(nil) +var _ SynApiInterface = (*WechatToBBRequest)(nil) + +type SynApiInterface interface { + GetSynNotice() *SynNotice +} + +type SynNotice struct { + OutBizBo string + Type SynNoticeType + BizContent SynApiInterface +} + +func (this *SynNotice) Marshal() ([]byte, error) { + return json.Marshal(this) +} diff --git a/internal/biz/kx/wx_to_bb.go b/internal/biz/kx/wx_to_bb.go new file mode 100644 index 0000000..38473bd --- /dev/null +++ b/internal/biz/kx/wx_to_bb.go @@ -0,0 +1,39 @@ +package kx + +// WechatToBBRequest 微信回调蓝色兄弟接口数据同步Api +type WechatToBBRequest struct { + // 活动ID + ActivityId string `protobuf:"bytes,9,opt,name=activityId,proto3" json:"activityId,omitempty"` + // 活动名称 + ActivityName string `protobuf:"bytes,10,opt,name=activityName,proto3" json:"activityName,omitempty"` + // 优惠券ID + VoucherId string `protobuf:"bytes,11,opt,name=voucherId,proto3" json:"voucherId,omitempty"` + // 领取用户ID + UserId string `protobuf:"bytes,12,opt,name=userId,proto3" json:"userId,omitempty"` + // 核销时间(Unix时间戳,毫秒) + UseTime string `protobuf:"bytes,13,opt,name=useTime,proto3" json:"useTime,omitempty"` + // 核销金额(分) + UseAmount string `protobuf:"bytes,14,opt,name=useAmount,proto3" json:"useAmount,omitempty"` + // 券消息类型,例如券核销(V_USE,V_REFUND) + BizType string `protobuf:"bytes,15,opt,name=bizType,proto3" json:"bizType,omitempty"` + // 退款时间(Unix时间戳,毫秒) + RefundTime string `protobuf:"bytes,16,opt,name=refundTime,proto3" json:"refundTime,omitempty"` + // 退款金额(分) + RefundAmount string `protobuf:"bytes,17,opt,name=refundAmount,proto3" json:"refundAmount,omitempty"` + // 券状态,可用(ENABLED)/不可用(DISABLED) + VoucherStatus string `protobuf:"bytes,18,opt,name=voucherStatus,proto3" json:"voucherStatus,omitempty"` + // 幂等ID + OrderId string `protobuf:"bytes,19,opt,name=orderId,proto3" json:"orderId,omitempty"` + // 支付宝交易号 + TradeNo string `protobuf:"bytes,20,opt,name=tradeNo,proto3" json:"tradeNo,omitempty"` + // 券领取时间(Unix时间戳,毫秒) + GmtVoucherCreate string `protobuf:"bytes,21,opt,name=gmtVoucherCreate,proto3" json:"gmtVoucherCreate,omitempty"` +} + +func (this *WechatToBBRequest) GetSynNotice() *SynNotice { + return &SynNotice{ + OutBizBo: this.OrderId, + Type: SynNoticeTypeBBToWechat, + BizContent: this, + } +} diff --git a/internal/biz/mixrepos/kx.go b/internal/biz/mixrepos/kx.go new file mode 100644 index 0000000..d3d17dc --- /dev/null +++ b/internal/biz/mixrepos/kx.go @@ -0,0 +1,10 @@ +package mixrepos + +import ( + "context" + "voucher/internal/biz/kx" +) + +type KxMixRepo interface { + Request(ctx context.Context, req *kx.SynNotice) error +} diff --git a/internal/biz/notify_tag.go b/internal/biz/notify_tag.go new file mode 100644 index 0000000..efe69c9 --- /dev/null +++ b/internal/biz/notify_tag.go @@ -0,0 +1,91 @@ +package biz + +import ( + "context" + "fmt" + "github.com/redis/go-redis/v9" + err2 "voucher/api/err" + "voucher/internal/biz/bo" + "voucher/internal/biz/vo" + "voucher/internal/pkg/lock" +) + +func (v *VoucherBiz) registerNotifyTag(ctx context.Context, stockCreatorMchID, stockID string) error { + + c := vo.WechatNotifyRegisterTagCacheKey.BuildCache([]string{v.bc.WechatNotifyMQ.Tag, stockCreatorMchID, stockID}) + + _, err := v.rdb.Rdb.Get(ctx, c.Key).Result() + + if err == nil { + // 缓存存在,直接返回 + return nil + } + + if err != redis.Nil { + return fmt.Errorf(fmt.Sprintf("获取redis缓存%s异常:%v", c.Key, err)) + } + + cl := vo.WechatNotifyRegisterTagCacheLockKey.BuildCache([]string{v.bc.WechatNotifyMQ.Tag, stockCreatorMchID, stockID}) + + return lock.NewMutex(v.rdb.Rdb, cl.TTL).Lock(ctx, cl.Key, func(ctx context.Context) error { + // 二次获取,判定处理,以免获取锁后又执行了一次 + + cacheValue, err3 := v.rdb.Rdb.Get(ctx, c.Key).Result() + + if err3 != nil && err3 != redis.Nil { + return fmt.Errorf(fmt.Sprintf("二次获取redis缓存%s异常:%v", c.Key, err)) + } + + if cacheValue != "" { + return nil // 有直接返回 + } + + wechatNotifyTag, err3 := v.WechatNotifyRegisterTagRepo.GetByStockIdAndMchId(ctx, stockCreatorMchID, stockID) + if err3 != nil && !err2.IsDbNotFound(err3) { + return err3 + } + + if wechatNotifyTag != nil { + if wechatNotifyTag.Tag != v.bc.WechatNotifyMQ.Tag { + return fmt.Errorf("tag不一致,请检查tag配置:%s", wechatNotifyTag.Tag) + } + + if wechatNotifyTag.Status.IsSuccess() { + return v.setCache(ctx, c, wechatNotifyTag) + } + } else { + wechatNotifyTag, err3 = v.createWechatNotifyRegisterTag(ctx, stockCreatorMchID, stockID) + if err3 != nil { + return err3 + } + } + + if err = v.WechatCpnRepo.RegisterNotifyTag(ctx, stockID); err != nil { + + return v.WechatNotifyRegisterTagRepo.Fail(ctx, wechatNotifyTag.ID, err.Error()) + } + + if err = v.WechatNotifyRegisterTagRepo.Success(ctx, wechatNotifyTag.ID); err != nil { + return err + } + + return v.setCache(ctx, c, wechatNotifyTag) + }) +} + +func (v *VoucherBiz) createWechatNotifyRegisterTag(ctx context.Context, stockCreatorMchID, stockID string) (*bo.WechatNotifyRegisterTagBo, error) { + return v.WechatNotifyRegisterTagRepo.Create(ctx, &bo.WechatNotifyRegisterTagBo{ + StockID: stockID, + StockCreatorMchID: stockCreatorMchID, + Tag: v.bc.WechatNotifyMQ.Tag, + }) +} + +func (v *VoucherBiz) setCache(ctx context.Context, c *vo.Cache, wechatNotifyTag *bo.WechatNotifyRegisterTagBo) error { + + if err := v.rdb.Rdb.Set(ctx, c.Key, wechatNotifyTag.Tag, c.TTL).Err(); err != nil { + return fmt.Errorf(fmt.Sprintf("设置redis缓存%s异常:%v", c.Key, err)) + } + + return nil +} diff --git a/internal/biz/order.go b/internal/biz/order.go index 8cccf2c..d3e3989 100644 --- a/internal/biz/order.go +++ b/internal/biz/order.go @@ -2,22 +2,97 @@ package biz import ( "context" + "encoding/json" "fmt" + "github.com/go-kratos/kratos/v2/errors" "github.com/go-kratos/kratos/v2/log" - "github.com/redis/go-redis/v9" err2 "voucher/api/err" + v1 "voucher/api/v1" "voucher/internal/biz/bo" "voucher/internal/biz/vo" - "voucher/internal/pkg/lock" ) -func (v *VoucherBiz) order(ctx context.Context, req *bo.OrderCreateReqBo, product *bo.ProductBo) (*bo.OrderBo, error) { +func (c *VoucherBiz) CmbOrder(ctx context.Context, request *v1.CmbRequest) (*v1.CmbReply, error) { + + order, err := c.cmbOrder(ctx, request) + + if err != nil { + return c.OrderFail(ctx, err) + } + + return c.OrderSuccess(ctx, order.OrderNo) +} + +func (c *VoucherBiz) cmbOrder(ctx context.Context, request *v1.CmbRequest) (*bo.OrderBo, error) { + + bizContent, err := c.CmbMixRepo.OrderVerify(ctx, request) + if err != nil { + return nil, err + } + + ctx2 := context.Background() + + boReq := &bo.OrderCreateReqBo{ + OutBizNo: bizContent.TransactionId, + ProductNo: bizContent.ActivityId, + Account: bizContent.CmbUid, + AppID: bizContent.AppId, + Attach: bizContent.Attach, + AccountType: vo.OrderAccountTypeOpenId, + Type: vo.OrderTypeCmb, + NotifyUrl: c.bc.Cmb.NotifyUrl, + } + + order, err := c.Order(ctx2, boReq, bizContent) + if err != nil { + return nil, err + } + + return order, nil +} + +func (v *VoucherBiz) Order(ctx context.Context, req *bo.OrderCreateReqBo, cmbReq *v1.CmbOrderRequest) (order *bo.OrderBo, err error) { + + order, err = v.OrderRepo.GetByOutBizNo(ctx, vo.OrderTypeCmb, req.OutBizNo) + + if err != nil && !err2.IsDbNotFound(err) { + return order, err + } + + if order != nil { + + if order.Status.IsFail() { + + if err4 := v.orderRetry(ctx, order); err4 != nil { + return order, err4 + } + } + + return order, err + } + + product, err3 := v.ProductRepo.GetByProductNo(ctx, req.ProductNo) + if err3 != nil { + return order, err3 + } + + order, err = v.order(ctx, req, product, cmbReq) + if err != nil { + return order, err + } + + return order, nil +} + +func (v *VoucherBiz) order(ctx context.Context, req *bo.OrderCreateReqBo, product *bo.ProductBo, cmbReq *v1.CmbOrderRequest) (*bo.OrderBo, error) { order, err := v.create(ctx, req, product) if err != nil { return nil, err } + // 通知kx + // 注册通知标签 order.MerchantNo 批次创建商户, order.BatchNo 商品批次号 if err = v.registerNotifyTag(ctx, order.MerchantNo, order.BatchNo); err != nil { return nil, err @@ -70,91 +145,12 @@ func (v *VoucherBiz) create(ctx context.Context, req *bo.OrderCreateReqBo, produ return v.OrderRepo.Create(ctx, o) } -func (v *VoucherBiz) registerNotifyTag(ctx context.Context, stockCreatorMchID, stockID string) error { - - c := vo.WechatNotifyRegisterTagCacheKey.BuildCache([]string{v.bc.WechatNotifyMQ.Tag, stockCreatorMchID, stockID}) - - _, err := v.rdb.Rdb.Get(ctx, c.Key).Result() - - if err == nil { - // 缓存存在,直接返回 - return nil - } - - if err != redis.Nil { - return fmt.Errorf(fmt.Sprintf("获取redis缓存%s异常:%v", c.Key, err)) - } - - cl := vo.WechatNotifyRegisterTagCacheLockKey.BuildCache([]string{v.bc.WechatNotifyMQ.Tag, stockCreatorMchID, stockID}) - - return lock.NewMutex(v.rdb.Rdb, cl.TTL).Lock(ctx, cl.Key, func(ctx context.Context) error { - // 二次获取,判定处理,以免获取锁后又执行了一次 - - cacheValue, err3 := v.rdb.Rdb.Get(ctx, c.Key).Result() - - if err3 != nil && err3 != redis.Nil { - return fmt.Errorf(fmt.Sprintf("二次获取redis缓存%s异常:%v", c.Key, err)) - } - - if cacheValue != "" { - return nil // 有直接返回 - } - - wechatNotifyTag, err3 := v.WechatNotifyRegisterTagRepo.GetByStockIdAndMchId(ctx, stockCreatorMchID, stockID) - if err3 != nil && !err2.IsDbNotFound(err3) { - return err3 - } - - if wechatNotifyTag != nil { - if wechatNotifyTag.Tag != v.bc.WechatNotifyMQ.Tag { - return fmt.Errorf("tag不一致,请检查tag配置:%s", wechatNotifyTag.Tag) - } - - if wechatNotifyTag.Status.IsSuccess() { - return v.setCache(ctx, c, wechatNotifyTag) - } - } else { - wechatNotifyTag, err3 = v.createWechatNotifyRegisterTag(ctx, stockCreatorMchID, stockID) - if err3 != nil { - return err3 - } - } - - if err = v.WechatCpnRepo.RegisterNotifyTag(ctx, stockID); err != nil { - - return v.WechatNotifyRegisterTagRepo.Fail(ctx, wechatNotifyTag.ID, err.Error()) - } - - if err = v.WechatNotifyRegisterTagRepo.Success(ctx, wechatNotifyTag.ID); err != nil { - return err - } - - return v.setCache(ctx, c, wechatNotifyTag) - }) -} - -func (v *VoucherBiz) createWechatNotifyRegisterTag(ctx context.Context, stockCreatorMchID, stockID string) (*bo.WechatNotifyRegisterTagBo, error) { - return v.WechatNotifyRegisterTagRepo.Create(ctx, &bo.WechatNotifyRegisterTagBo{ - StockID: stockID, - StockCreatorMchID: stockCreatorMchID, - Tag: v.bc.WechatNotifyMQ.Tag, - }) -} - -func (v *VoucherBiz) setCache(ctx context.Context, c *vo.Cache, wechatNotifyTag *bo.WechatNotifyRegisterTagBo) error { - - if err := v.rdb.Rdb.Set(ctx, c.Key, wechatNotifyTag.Tag, c.TTL).Err(); err != nil { - return fmt.Errorf(fmt.Sprintf("设置redis缓存%s异常:%v", c.Key, err)) - } - - return nil -} - func (v *VoucherBiz) ing(ctx context.Context, id uint64) error { return v.OrderRepo.Ing(ctx, id) } func (v *VoucherBiz) success(ctx context.Context, order *bo.OrderBo, voucherNo string) error { + order.VoucherNo = voucherNo return v.OrderRepo.Success(ctx, order.ID, voucherNo) } @@ -171,115 +167,37 @@ func (v *VoucherBiz) fail(ctx context.Context, order *bo.OrderBo, errReq error) return v.alarm(ctx, order, errReq.Error()) } -func (v *VoucherBiz) alarm(ctx context.Context, order *bo.OrderBo, errMsg string) error { +func (c *VoucherBiz) OrderSuccess(ctx context.Context, orderNo string) (*v1.CmbReply, error) { - // 1小时 内 指定的批次号 发放 发生错误 预警 - c := vo.OrderConsumeFailAlarmKey.BuildCache([]string{order.ProductNo}) - - _, err := v.rdb.Rdb.Get(ctx, c.Key).Result() - - if err == nil { - // 缓存存在,直接返回 - return nil + bizReply := &v1.CmbOrderReply{ + RespCode: vo.CmbResponseStatusSuccess.GetValue(), + RespMsg: "成功", + CodeNo: orderNo, } - if err != redis.Nil { - return fmt.Errorf(fmt.Sprintf("alarm 获取redis缓存%s异常:%v", c.Key, err)) - } + replyBizContent, _ := json.Marshal(bizReply) - cl := vo.OrderConsumeFailAlarmLockKey.BuildCache([]string{order.ProductNo}) - - return lock.NewMutex(v.rdb.Rdb, cl.TTL).Lock(ctx, cl.Key, func(ctx context.Context) error { - // 二次获取,判定处理,以免获取锁后又执行了一次 - - cacheValue, err3 := v.rdb.Rdb.Get(ctx, c.Key).Result() - - if err3 != nil && err3 != redis.Nil { - return fmt.Errorf(fmt.Sprintf("alarm 二次获取redis缓存%s异常:%v", c.Key, err)) - } - - if len(cacheValue) > 0 { - return nil // 有直接返回 - } - - // 通知 - if err = v.DingMixRepo.SendMarkdownMessage(ctx, "异常通知", v.alarmText(ctx, order, errMsg)); err != nil { - return err - } - - if err = v.rdb.Rdb.Set(ctx, c.Key, order.ProductNo, c.TTL).Err(); err != nil { - return fmt.Errorf(fmt.Sprintf("设置redis缓存%s异常:%v", c.Key, err)) - } - - return nil - }) + return c.GetResponse(ctx, replyBizContent) } -func (v *VoucherBiz) alarmText(_ context.Context, order *bo.OrderBo, errMsg string) string { +func (c *VoucherBiz) OrderFail(ctx context.Context, err error) (*v1.CmbReply, error) { - remarks := fmt.Sprintf("订单号:%s,商品编号:%s,原因:%s", order.OrderNo, order.ProductNo, errMsg) + se := errors.FromError(err) - msg := "# " + - "

立减金发放平台报警通知

" + - "
\n" + - "" + - "不好了,订单发放发生异常了" + - "[%s]请尽快处理@相关人员。" + - "" + if len(se.Reason) == 0 { + se.Reason = err2.CmbErr_CMB_UNKNOWN.String() + } - return fmt.Sprintf(msg, remarks) -} - -func (v *VoucherBiz) Query(ctx context.Context, order *bo.OrderBo) error { - - status, err := v.WechatCpnRepo.Query(ctx, order) - if err != nil { - return err - } - - if order.Status == status { - log.Warnf("券状态未改变:%s,忽略不处理,orderNo:%s", order.Status.GetText(), order.OrderNo) - return nil - } - - if err = v.UpdateOrderStatus(ctx, order.ID, status); err != nil { - return err - } - - order.Status = status - - return nil -} - -func (v *VoucherBiz) UpdateOrderStatus(ctx context.Context, orderId uint64, status vo.OrderStatus) error { - - if status.IsSuccess() { - - return v.OrderRepo.Available(ctx, orderId) - - } else if status.IsUse() { - - return v.OrderRepo.Used(ctx, orderId) - - } else if status.IsExpired() { - - return v.OrderRepo.Expired(ctx, orderId) - } - - return fmt.Errorf("notice 未知券状态,orderId:%d,statuText:%s", orderId, status.GetText()) -} - -func (v *VoucherBiz) QueryOrder(ctx context.Context, orderNo string) (string, error) { - - order, err3 := v.OrderRepo.GetByOrderNo(ctx, orderNo) - if err3 != nil { - return "", err3 - } - - status, err := v.WechatCpnRepo.Query(ctx, order) - if err != nil { - return "", err - } - - return fmt.Sprintf("orderNo:%s,订单状态:%s,微信查询返回状态:%s", orderNo, order.Status.GetText(), status.GetText()), nil + log.Errorf("order fail: %v", se) + + bizReply := &v1.CmbOrderReply{ + RespCode: vo.CmbResponseStatusFail.GetValue(), + RespMsg: se.Message, + CodeNo: "", + ThirdErrCode: se.Reason, + } + + replyBizContent, _ := json.Marshal(bizReply) + + return c.GetResponse(ctx, replyBizContent) } diff --git a/internal/biz/product.go b/internal/biz/product.go new file mode 100644 index 0000000..9d62b66 --- /dev/null +++ b/internal/biz/product.go @@ -0,0 +1,80 @@ +package biz + +import ( + "context" + "fmt" + "time" + v1 "voucher/api/v1" + "voucher/internal/biz/vo" + "voucher/internal/pkg/lock" +) + +func (v *VoucherBiz) CmbProductQuery(ctx context.Context, productNo string) (reps *v1.CmbQueryProductReply, err error) { + + c := vo.CmbProductQueryLockKey.BuildCache([]string{productNo}) + + err = lock.NewMutex(v.rdb.Rdb, c.TTL).Lock(ctx, c.Key, func(ctx context.Context) error { + + product, err3 := v.ProductRepo.GetByProductNo(ctx, productNo) + if err3 != nil { + return err3 + } + + if !product.Channel.IsWeChat() { + return fmt.Errorf("只支持微信") + } + + wechatResp, err4 := v.WechatCpnRepo.QueryProduct(ctx, product.MchId, product.BatchNo) + if err4 != nil { + return err4 + } + + reps = &v1.CmbQueryProductReply{ + RespCode: vo.CmbResponseStatusSuccess.GetValue(), + RespMsg: "成功", + ActivityName: product.Name, + ActivityId: product.ProductNo, + Amount: "", + MinAmount: "", + AvailableType: "", + AvailableDays: "", // 动态有效期天数 + StartTime: "", + EndTime: "", + AvailableStock: "", + Detail: *wechatResp.Description, + } + + inputFormat := time.RFC3339 + + if wechatResp.AvailableBeginTime != nil { + + availableBeginTime, _ := time.Parse(inputFormat, *wechatResp.AvailableBeginTime) + reps.StartTime = availableBeginTime.Format("2006-01-02 15:04:05.000") + reps.SaleStartTime = reps.StartTime + } + + if wechatResp.AvailableEndTime != nil { + availableEndTime, _ := time.Parse(inputFormat, *wechatResp.AvailableEndTime) + reps.EndTime = availableEndTime.Format("2006-01-02 15:04:05.000") + reps.SaleEndTime = reps.EndTime + } + + reps.Amount = fmt.Sprintf("%d", *wechatResp.StockUseRule.FixedNormalCoupon.CouponAmount) + reps.MinAmount = fmt.Sprintf("%d", *wechatResp.StockUseRule.FixedNormalCoupon.TransactionMinimum) + + availableStock := *wechatResp.StockUseRule.MaxCoupons - *wechatResp.DistributedCoupons + reps.AvailableStock = fmt.Sprintf("%d", availableStock) + + availableType, err3 := product.AvailableType.GetCmbAvailableType() + if err3 != nil { + return err3 + } + + reps.AvailableType = availableType.GetValue() + reps.AvailableDays = fmt.Sprintf("%d", product.AvailableDays) + + return nil + }) + + return +} diff --git a/internal/biz/query.go b/internal/biz/query.go new file mode 100644 index 0000000..dd0463f --- /dev/null +++ b/internal/biz/query.go @@ -0,0 +1,100 @@ +package biz + +import ( + "context" + "fmt" + "github.com/go-kratos/kratos/v2/log" + "time" + v1 "voucher/api/v1" + "voucher/internal/biz/bo" + "voucher/internal/biz/vo" + "voucher/internal/pkg/lock" +) + +func (v *VoucherBiz) CmbQuery(ctx context.Context, orderNo string) (resp *v1.CmbQueryReply, err error) { + + c := vo.CmbQueryLockKey.BuildCache([]string{orderNo}) + + err = lock.NewMutex(v.rdb.Rdb, c.TTL).Lock(ctx, c.Key, func(ctx context.Context) error { + + order, err3 := v.OrderRepo.GetByOrderNo(ctx, orderNo) + if err3 != nil { + return err3 + } + + if err = v.Query(ctx, order); err != nil { + return err + } + + status, err3 := order.Status.GetCmbStatusText() + if err3 != nil { + return err3 + } + + resp = &v1.CmbQueryReply{ + Ticket: order.OrderNo, + Status: status.GetValue(), + TransDate: time.Now().Format("20060102150405"), + OrgNo: v.bc.Cmb.OrgNo, + Ext: "", + } + + return nil + }) + + return +} + +func (v *VoucherBiz) Query(ctx context.Context, order *bo.OrderBo) error { + + status, err := v.WechatCpnRepo.Query(ctx, order) + if err != nil { + return err + } + + if order.Status == status { + log.Warnf("券状态未改变:%s,忽略不处理,orderNo:%s", order.Status.GetText(), order.OrderNo) + return nil + } + + if err = v.UpdateOrderStatus(ctx, order.ID, status); err != nil { + return err + } + + order.Status = status + + return nil +} + +func (v *VoucherBiz) UpdateOrderStatus(ctx context.Context, orderId uint64, status vo.OrderStatus) error { + + if status.IsSuccess() { + + return v.OrderRepo.Available(ctx, orderId) + + } else if status.IsUse() { + + return v.OrderRepo.Used(ctx, orderId) + + } else if status.IsExpired() { + + return v.OrderRepo.Expired(ctx, orderId) + } + + return fmt.Errorf("notice 未知券状态,orderId:%d,statuText:%s", orderId, status.GetText()) +} + +func (v *VoucherBiz) QueryOrder(ctx context.Context, orderNo string) (string, error) { + + order, err3 := v.OrderRepo.GetByOrderNo(ctx, orderNo) + if err3 != nil { + return "", err3 + } + + status, err := v.WechatCpnRepo.Query(ctx, order) + if err != nil { + return "", err + } + + return fmt.Sprintf("orderNo:%s,订单状态:%s,微信查询返回状态:%s", orderNo, order.Status.GetText(), status.GetText()), nil +} diff --git a/internal/biz/voucher.go b/internal/biz/voucher.go index de5de99..be8443a 100644 --- a/internal/biz/voucher.go +++ b/internal/biz/voucher.go @@ -1,9 +1,14 @@ package biz import ( + "context" + "github.com/go-kratos/kratos/v2/log" + v1 "voucher/api/v1" + "voucher/internal/biz/bo" "voucher/internal/biz/cmb" "voucher/internal/biz/mixrepos" "voucher/internal/biz/repo" + "voucher/internal/biz/vo" "voucher/internal/biz/wechatrepo" "voucher/internal/conf" "voucher/internal/data" @@ -22,6 +27,7 @@ type VoucherBiz struct { WechatCpnRepo wechatrepo.WechatCpnRepo DingMixRepo mixrepos.DingMixRepo CmbMixRepo mixrepos.CmbMixRepo + KxMixRepo mixrepos.KxMixRepo } func NewVoucherBiz( @@ -37,6 +43,7 @@ func NewVoucherBiz( WechatCpnRepo wechatrepo.WechatCpnRepo, DingMixRepo mixrepos.DingMixRepo, CmbMixRepo mixrepos.CmbMixRepo, + KxMixRepo mixrepos.KxMixRepo, ) *VoucherBiz { return &VoucherBiz{ bc: bc, @@ -51,5 +58,23 @@ func NewVoucherBiz( WechatCpnRepo: WechatCpnRepo, DingMixRepo: DingMixRepo, CmbMixRepo: CmbMixRepo, + KxMixRepo: KxMixRepo, } } + +func (c *VoucherBiz) GetResponse(ctx context.Context, replyBizContent []byte) (*v1.CmbReply, error) { + + req := &bo.CmbResponseBo{ + RespCode: vo.CmbResponseStatusSuccess.GetValue(), + RespMsg: "成功", + BizContent: string(replyBizContent), + } + + reply, err := c.CmbMixRepo.GetResponse(ctx, req) + if err != nil { + log.Errorf("build cmb response fail: %v", err) + return nil, err + } + + return reply, nil +} diff --git a/internal/data/mixrepoimpl/kx.go b/internal/data/mixrepoimpl/kx.go new file mode 100644 index 0000000..141e58d --- /dev/null +++ b/internal/data/mixrepoimpl/kx.go @@ -0,0 +1,58 @@ +package mixrepoimpl + +import ( + "context" + "encoding/json" + "fmt" + "github.com/go-kratos/kratos/v2/log" + "net/http" + "time" + v1 "voucher/api/v1" + "voucher/internal/biz/kx" + "voucher/internal/biz/mixrepos" + "voucher/internal/biz/vo" + "voucher/internal/conf" + "voucher/internal/pkg/request" +) + +type KxMixRepoImpl struct { + bc *conf.Bootstrap +} + +func NewKxMixRepoImpl(bc *conf.Bootstrap) mixrepos.KxMixRepo { + return &KxMixRepoImpl{bc: bc} +} + +func (s *KxMixRepoImpl) Request(ctx context.Context, req *kx.SynNotice) error { + + body, err := req.Marshal() + if err != nil { + log.Errorf("请求掌上生活Marshal报错:%s", err.Error()) + return err + } + + h := http.Header{ + "Content-Type": []string{"application/x-www-form-urlencoded"}, + } + + url := "" + + _, bodyBytes, err := request.Post(ctx, url, body, request.WithHeaders(h), request.WithTimeout(time.Second*20)) + if err != nil { + log.Errorf("请求kx报错,url:%s,err:%v", url, err) + return err + } + + var response *v1.CmbReply + if err = json.Unmarshal(bodyBytes, &response); err != nil { + log.Errorf("请求kx返回数据解析报错:%s,url:%s,bodyBytes:%s", err.Error(), url, string(bodyBytes)) + return err + } + + if response.RespCode != vo.CmbResponseStatusSuccess.GetValue() { + log.Errorf("请求kx返回报错:msg:%s,url:%s,bodyBytes:%s", response.RespMsg, url, string(bodyBytes)) + return fmt.Errorf(response.RespMsg) + } + + return nil +} diff --git a/internal/data/mixrepoimpl/provider_set.go b/internal/data/mixrepoimpl/provider_set.go index 9a3ba66..a74a74c 100644 --- a/internal/data/mixrepoimpl/provider_set.go +++ b/internal/data/mixrepoimpl/provider_set.go @@ -10,4 +10,5 @@ var ProviderMixRepoImplSet = wire.NewSet( NewMQSendMixRepoImpl, NewCmbMixRepoImpl, NewDingMixRepoImpl, + NewKxMixRepoImpl, ) diff --git a/internal/service/cmb_order.go b/internal/service/cmb_order.go index 3351c1f..33bc3a0 100644 --- a/internal/service/cmb_order.go +++ b/internal/service/cmb_order.go @@ -2,85 +2,10 @@ package service import ( "context" - "encoding/json" - "github.com/go-kratos/kratos/v2/errors" - "github.com/go-kratos/kratos/v2/log" - err2 "voucher/api/err" v1 "voucher/api/v1" - "voucher/internal/biz/bo" - "voucher/internal/biz/vo" ) -func (c *CmbService) OrderSuccess(ctx context.Context, orderNo string) (*v1.CmbReply, error) { - - bizReply := &v1.CmbOrderReply{ - RespCode: vo.CmbResponseStatusSuccess.GetValue(), - RespMsg: "成功", - CodeNo: orderNo, - } - - replyBizContent, _ := json.Marshal(bizReply) - - return c.GetResponse(ctx, replyBizContent) -} - -func (c *CmbService) OrderFail(ctx context.Context, err error) (*v1.CmbReply, error) { - - se := errors.FromError(err) - - if len(se.Reason) == 0 { - se.Reason = err2.CmbErr_CMB_UNKNOWN.String() - } - - log.Errorf("order fail: %v", se) - - bizReply := &v1.CmbOrderReply{ - RespCode: vo.CmbResponseStatusFail.GetValue(), - RespMsg: se.Message, - CodeNo: "", - ThirdErrCode: se.Reason, - } - - replyBizContent, _ := json.Marshal(bizReply) - - return c.GetResponse(ctx, replyBizContent) -} - func (c *CmbService) Order(ctx context.Context, request *v1.CmbRequest) (*v1.CmbReply, error) { - orderNo, err := c.order(ctx, request) - - if err != nil { - return c.OrderFail(ctx, err) - } - - return c.OrderSuccess(ctx, orderNo) -} - -func (c *CmbService) order(ctx context.Context, request *v1.CmbRequest) (string, error) { - - bizContent, err := c.CmbMixRepo.OrderVerify(ctx, request) - if err != nil { - return "", err - } - - ctx2 := context.Background() - - boReq := &bo.OrderCreateReqBo{ - OutBizNo: bizContent.TransactionId, - ProductNo: bizContent.ActivityId, - Account: bizContent.CmbUid, - AppID: bizContent.AppId, - Attach: bizContent.Attach, - AccountType: vo.OrderAccountTypeOpenId, - Type: vo.OrderTypeCmb, - NotifyUrl: c.bc.Cmb.NotifyUrl, - } - - orderNo, err := c.VoucherBiz.CmbOrder(ctx2, boReq) - if err != nil { - return "", err - } - - return orderNo, nil + return c.VoucherBiz.CmbOrder(ctx, request) }