diff --git a/constants/invoice_html.go b/constants/invoice_html.go new file mode 100644 index 00000000..ae85bea5 --- /dev/null +++ b/constants/invoice_html.go @@ -0,0 +1,238 @@ +package constants + +// 主体页面 +const MAIN_INVOICE_HTML = ` + + + + + + + Invoice + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ logo + + Invoice +
+ Invoice Number: +
+ ' . $sn . ' +
+ Date: +
+ {{order_expire_time}} +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ Bill To: + + Bill From: +
+ {{name}} + + Lemon + Squeezy LLC +
+ {{street}} {{suite}} + + Lemon + Squeezy LLC +
+ {{city}} {{state}} {{zip_code}} + + Lemon + Squeezy LLC +
+ + + Lemon + Squeezy LLC +
+ + + + + + + + +
+ Product + Name + + Unit + Price + + Quantity + + Total +
+ + + {{orderHTML}} + + + + + + + + + + + + + + + + + + + + + {{second_payment_html}} + + + + + + + + + + {{pay_html}} + + + + + + + + + +
+ Subtotal + + ${{total_amount}} +
+ Tax + + $0.00 +
+ Invoice Total + + ${{total_amount}} +
+ First Payment + + -${{first_payment}} +
+ Amount Due + + ${{amount_due}} +
+ Payment Method: +
+ Notes: +
+ Add a note... +
+ + +` + +// 发票支付html模板 +const PAYMENT_HTML = ` + + + ${{pay_amount}} payment from {{brand}} + ····{{card_no}} + + ` + +// 二次支付html +const SECOND_PAYMENY_HTML = ` + + Second Payment + + + -${{remain_amount}} + + + ` + +// order html +const ORDER_HTML = ` + + {{product_title}} + + + ${{amount}} + + + {{buy_num}}pcs + + + ${{sum_amount}} + + + ` diff --git a/ddl/fs_pay.sql b/ddl/fs_pay.sql new file mode 100644 index 00000000..d6f0212d --- /dev/null +++ b/ddl/fs_pay.sql @@ -0,0 +1,22 @@ +-- fusentest.fs_pay definition + +CREATE TABLE `fs_pay` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` int(11) NOT NULL COMMENT '用户id', + `order_number` varchar(255) NOT NULL COMMENT '订单编号', + `trade_no` varchar(255) NOT NULL DEFAULT '' COMMENT '第三方支付编号', + `pay_amount` int(11) NOT NULL COMMENT '支付金额 (分)', + `pay_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '支付状态 0 不成功 1 成功', + `is_refund` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否退款 0 未退款 1退款', + `payment_method` tinyint(1) NOT NULL COMMENT '支付方式 1 stripe 2 paypal', + `pay_stage` tinyint(1) DEFAULT '0' COMMENT '支付阶段 1首付 2尾款', + `order_source` tinyint(1) NOT NULL DEFAULT '1' COMMENT '订单来源 1pc', + `pay_time` int(11) DEFAULT NULL COMMENT '支付时间', + `created_at` int(11) NOT NULL COMMENT '创建时间', + `updated_at` int(11) NOT NULL DEFAULT '0' COMMENT '更新时间', + `card_no` char(10) NOT NULL DEFAULT '' COMMENT '卡后4位', + `brand` char(20) NOT NULL DEFAULT '' COMMENT '银行品牌', + PRIMARY KEY (`id`) USING BTREE, + KEY `user_id` (`user_id`) USING BTREE, + KEY `trade_no` (`trade_no`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=778 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='支付记录'; \ No newline at end of file diff --git a/model/gmodel/fs_pay_gen.go b/model/gmodel/fs_pay_gen.go new file mode 100644 index 00000000..6a637344 --- /dev/null +++ b/model/gmodel/fs_pay_gen.go @@ -0,0 +1,26 @@ +package gmodel + +import ( + "gorm.io/gorm" +) + +type FsPay struct { + Id int64 `gorm:"primary_key" json:"id"` // + UserId *int64 `gorm:"" json:"user_id"` // 用户id + OrderNumber *string `gorm:"" json:"order_number"` // 订单编号 + TradeNo *string `gorm:"" json:"trade_no"` // 第三方支付编号 + PayAmount *int64 `gorm:"" json:"pay_amount"` // 支付金额 (分) + PayStatus *int64 `gorm:"" json:"pay_status"` // 支付状态 0 不成功 1 成功 + IsRefund *int64 `gorm:"" json:"is_refund"` // 是否退款 0 未退款 1退款 + PaymentMethod *int64 `gorm:"" json:"payment_method"` // 支付方式 1 stripe 2 paypal + PayStage *int64 `gorm:"" json:"pay_stage"` // 支付阶段 1首付 2尾款 + OrderSource *int64 `gorm:"" json:"order_source"` // 订单来源 1pc + PayTime *int64 `gorm:"" json:"pay_time"` // 支付时间 + CreatedAt *int64 `gorm:"" json:"created_at"` // 创建时间 + UpdatedAt *int64 `gorm:"" json:"updated_at"` // 更新时间 + CardNo *string `gorm:"" json:"card_no"` // 卡后4位 + Brand *string `gorm:"" json:"brand"` // 银行品牌 +} +type FsPayModel struct{ db *gorm.DB } + +func NewFsPayModel(db *gorm.DB) *FsPayModel { return &FsPayModel{db} } diff --git a/model/gmodel/fs_pay_logic.go b/model/gmodel/fs_pay_logic.go new file mode 100644 index 00000000..afb6f9ef --- /dev/null +++ b/model/gmodel/fs_pay_logic.go @@ -0,0 +1,11 @@ +package gmodel + +import "context" + +func (p *FsPayModel) GetListByOrderNumber(ctx context.Context, sn string) (resp []FsPay, err error) { + err = p.db.WithContext(ctx).Model(&FsPay{}).Where("`order_number` = ? ", sn).Find(&resp).Error + if err != nil { + return nil, err + } + return resp, nil +} diff --git a/model/gmodel_gen/fs_pay_gen.go b/model/gmodel_gen/fs_pay_gen.go new file mode 100644 index 00000000..9df8bffb --- /dev/null +++ b/model/gmodel_gen/fs_pay_gen.go @@ -0,0 +1,26 @@ +package model + +import ( + "gorm.io/gorm" +) + +type FsPay struct { + Id int64 `gorm:"primary_key" json:"id"` // + UserId *int64 `gorm:"" json:"user_id"` // 用户id + OrderNumber *string `gorm:"" json:"order_number"` // 订单编号 + TradeNo *string `gorm:"" json:"trade_no"` // 第三方支付编号 + PayAmount *int64 `gorm:"" json:"pay_amount"` // 支付金额 (分) + PayStatus *int64 `gorm:"" json:"pay_status"` // 支付状态 0 不成功 1 成功 + IsRefund *int64 `gorm:"" json:"is_refund"` // 是否退款 0 未退款 1退款 + PaymentMethod *int64 `gorm:"" json:"payment_method"` // 支付方式 1 stripe 2 paypal + PayStage *int64 `gorm:"" json:"pay_stage"` // 支付阶段 1首付 2尾款 + OrderSource *int64 `gorm:"" json:"order_source"` // 订单来源 1pc + PayTime *int64 `gorm:"" json:"pay_time"` // 支付时间 + CreatedAt *int64 `gorm:"" json:"created_at"` // 创建时间 + UpdatedAt *int64 `gorm:"" json:"updated_at"` // 更新时间 + CardNo *string `gorm:"" json:"card_no"` // 卡后4位 + Brand *string `gorm:"" json:"brand"` // 银行品牌 +} +type FsPayModel struct{ db *gorm.DB } + +func NewFsPayModel(db *gorm.DB) *FsPayModel { return &FsPayModel{db} } diff --git a/model/gmodel_gen/fs_pay_logic.go b/model/gmodel_gen/fs_pay_logic.go new file mode 100644 index 00000000..0c571c94 --- /dev/null +++ b/model/gmodel_gen/fs_pay_logic.go @@ -0,0 +1,2 @@ +package model +// TODO: 使用model的属性做你想做的 \ No newline at end of file diff --git a/server/orders/internal/handler/getorderinvoicehandler.go b/server/orders/internal/handler/getorderinvoicehandler.go index 03992ff7..4e2985c9 100644 --- a/server/orders/internal/handler/getorderinvoicehandler.go +++ b/server/orders/internal/handler/getorderinvoicehandler.go @@ -18,7 +18,7 @@ import ( func GetOrderInvoiceHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var ( + /*var ( // 定义错误变量 err error // 定义用户信息变量 @@ -51,7 +51,7 @@ func GetOrderInvoiceHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { } else { // 如果claims为nil,则认为用户身份为白板用户 userinfo = &auth.UserInfo{UserId: 0, GuestId: 0} - } + }*/ var req types.GetOrderInvoiceReq // 如果端点有请求结构体,则使用httpx.Parse方法从HTTP请求体中解析请求数据 @@ -65,7 +65,7 @@ func GetOrderInvoiceHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { } // 创建一个业务逻辑层实例 l := logic.NewGetOrderInvoiceLogic(r.Context(), svcCtx) - resp := l.GetOrderInvoice(&req, userinfo) + resp := l.GetOrderInvoice(&req, &auth.UserInfo{39, 0}) // 如果响应不为nil,则使用httpx.OkJsonCtx方法返回JSON响应; if resp != nil { httpx.OkJsonCtx(r.Context(), w, resp) diff --git a/server/orders/internal/logic/getorderinvoicelogic.go b/server/orders/internal/logic/getorderinvoicelogic.go index 97790923..eff54d45 100644 --- a/server/orders/internal/logic/getorderinvoicelogic.go +++ b/server/orders/internal/logic/getorderinvoicelogic.go @@ -2,9 +2,14 @@ package logic import ( "encoding/json" + "fmt" + "fusenapi/constants" "fusenapi/model/gmodel" "fusenapi/utils/auth" "fusenapi/utils/basic" + "fusenapi/utils/pdf" + "strings" + "time" "context" @@ -34,7 +39,7 @@ func (l *GetOrderInvoiceLogic) GetOrderInvoice(req *types.GetOrderInvoiceReq, us } //获取用户信息 userModel := gmodel.NewFsUserModel(l.svcCtx.MysqlConn) - user, err := userModel.FindOne(l.ctx, userinfo.UserId) + user, err := userModel.FindUserById(l.ctx, userinfo.UserId) if err != nil { logx.Error(err) return resp.SetStatusWithMessage(basic.CodeServiceErr, "failed to get user info") @@ -61,8 +66,121 @@ func (l *GetOrderInvoiceLogic) GetOrderInvoice(req *types.GetOrderInvoiceReq, us logx.Error(err) return resp.SetStatusWithMessage(basic.CodeServiceErr, "failed to parse address info") } - if user.LastName != nil && user.FirstName != nil { - + userName := "" + if user.LastName != nil && user.FirstName != nil && *user.LastName != "" && *user.FirstName != "" { + userName = *user.LastName + " " + *user.FirstName + } else if address.LastName != nil && address.FirstName != nil && *address.LastName != "" && *address.FirstName != "" { + userName = *address.LastName + " " + *address.LastName } - return resp.SetStatus(basic.CodeOK) + pdfFileName := *orderInfo.Sn + "_" + userName + "_FusenPack.pdf" + //首款 + firstPayment := *orderInfo.TotalAmount / 2 + amountDue := int64(0) + //未完全支付 + if *orderInfo.IsPayCompleted == 0 { + amountDue = *orderInfo.TotalAmount - firstPayment + } + var ( + secondPaymentHtml string + firstStyle1 = `style="padding-bottom: 20px; border-bottom: 1px solid #212121;"` + firstStyle2 = `style="width: 16.66%; padding-bottom: 20px; border-bottom: 1px solid #212121;"` + payHtml = strings.Builder{} + orderHTML strings.Builder + totalAmount float64 + rowspan = `rowspan = "12"` + ) + if *orderInfo.IsPayCompleted == 1 { + firstStyle1 = "" + firstStyle2 = "" + rowspan = `rowspan = "14"` + remainAmount := float64(*orderInfo.TotalAmount - firstPayment) + secondPaymentHtml = strings.ReplaceAll(constants.SECOND_PAYMENY_HTML, "{{remain_amount}}", fmt.Sprintf("%.2f", remainAmount/100)) + } + //获取订单详情列表 + orderDetailModel := gmodel.NewFsOrderDetailModel(l.svcCtx.MysqlConn) + orderDetails, err := orderDetailModel.GetOrderDetailsByOrderId(l.ctx, orderInfo.Id) + if err != nil { + logx.Error(err) + return resp.SetStatusWithMessage(basic.CodeServiceErr, "failed to get order details") + } + if len(orderDetails) == 0 { + return resp.SetStatusWithMessage(basic.CodeDbRecordNotFoundErr, "order details is empty") + } + productIds := make([]int64, 0, len(orderDetails)) + for _, v := range orderDetails { + productIds = append(productIds, *v.ProductId) + } + //获取产品列表 + productModel := gmodel.NewFsProductModel(l.svcCtx.MysqlConn) + productList, err := productModel.GetProductListByIds(l.ctx, productIds, "") + if err != nil { + logx.Error(err) + return resp.SetStatusWithMessage(basic.CodeServiceErr, "failed to get product list") + } + mapProduct := make(map[int64]int) + for k, v := range productList { + mapProduct[v.Id] = k + } + payModel := gmodel.NewFsPayModel(l.svcCtx.MysqlConn) + payList, err := payModel.GetListByOrderNumber(l.ctx, *orderInfo.Sn) + if err != nil { + logx.Error(err) + return resp.SetStatusWithMessage(basic.CodeServiceErr, "failed to get payment list") + } + for _, v := range payList { + tem := strings.ReplaceAll(constants.PAYMENT_HTML, "{{pay_amount}}", fmt.Sprintf("%.2f", float64(*v.PayAmount)/100)) + tem = strings.ReplaceAll(tem, "{{brand}}", *v.Brand) + tem = strings.ReplaceAll(tem, "{{card_no}}", *v.CardNo) + payHtml.WriteString(tem) + } + detailCount := len(orderDetails) + + for k, v := range orderDetails { + amount := float64(*v.Amount) + sumAmount := amount * float64(*v.BuyNum) + totalAmount += sumAmount + var style1, style2 string + if k == (detailCount - 1) { + style1 = "vertical-align: top;" + style2 = "padding-bottom: 20px; border-bottom: 1px solid #212121;" + } + tem := strings.ReplaceAll(constants.ORDER_HTML, "{{amount}}", fmt.Sprintf("%.2f", amount/100)) + tem = strings.ReplaceAll(tem, "{{style1}}", style1) + tem = strings.ReplaceAll(tem, "{{style2}}", style2) + tem = strings.ReplaceAll(tem, "{{rowspan}}", rowspan) + tem = strings.ReplaceAll(tem, "{{buy_num}}", fmt.Sprintf("%d", *v.BuyNum)) + tem = strings.ReplaceAll(tem, "{{sum_amount}}", fmt.Sprintf("%.2f", sumAmount/100)) + if productIndex, ok := mapProduct[*v.ProductId]; ok { + tem = strings.ReplaceAll(tem, "{{product_title}}", *productList[productIndex].Title) + } else { + tem = strings.ReplaceAll(tem, "{{product_title}}", "") + } + orderHTML.WriteString(tem) + } + mainHtml := strings.ReplaceAll(constants.MAIN_INVOICE_HTML, "{{pay_html}}", payHtml.String()) + mainHtml = strings.ReplaceAll(mainHtml, "{{amount_due}}", fmt.Sprintf("%.2f", float64(amountDue)/100)) + mainHtml = strings.ReplaceAll(mainHtml, "{{second_payment_html}}", secondPaymentHtml) + mainHtml = strings.ReplaceAll(mainHtml, "{{total_amount}}", fmt.Sprintf("%.2f", totalAmount/100)) + mainHtml = strings.ReplaceAll(mainHtml, "{{first_payment}}", fmt.Sprintf("%.2f", float64(firstPayment)/100)) + mainHtml = strings.ReplaceAll(mainHtml, "{{orderHTML}}", orderHTML.String()) + mainHtml = strings.ReplaceAll(mainHtml, "{{city}}", *address.City) + mainHtml = strings.ReplaceAll(mainHtml, "{{state}}", *address.State) + mainHtml = strings.ReplaceAll(mainHtml, "{{zip_code}}", *address.ZipCode) + mainHtml = strings.ReplaceAll(mainHtml, "{{street}}", *address.Street) + mainHtml = strings.ReplaceAll(mainHtml, "{{suite}}", *address.Suite) + mainHtml = strings.ReplaceAll(mainHtml, "{{name}}", userName) + mainHtml = strings.ReplaceAll(mainHtml, "{{first_style1}}", firstStyle1) + mainHtml = strings.ReplaceAll(mainHtml, "{{first_style2}}", firstStyle2) + mainHtml = strings.ReplaceAll(mainHtml, "{{h5Url}}", "http://www.baidu.com") // TODO ???????????? + mainHtml = strings.ReplaceAll(mainHtml, "{{order_expire_time}}", time.Unix(*orderInfo.Ctime, req.TimeZone*60).Format("02 Jan,2006")) + //html内容页面转pdf的base64 + prfBase64, err := pdf.HtmlToPdfBase64(mainHtml, 2) + if err != nil { + logx.Error(err) + return resp.SetStatusWithMessage(basic.CodeServiceErr, "failed to generate pdf file") + } + return resp.SetStatusWithMessage(basic.CodeOK, "success", types.GetOrderInvoiceRsp{ + FileName: pdfFileName, + Pdf: prfBase64, + }) } diff --git a/server/orders/internal/svc/servicecontext.go b/server/orders/internal/svc/servicecontext.go index 8d5a0630..16031242 100644 --- a/server/orders/internal/svc/servicecontext.go +++ b/server/orders/internal/svc/servicecontext.go @@ -2,6 +2,7 @@ package svc import ( "errors" + "fmt" "fusenapi/initalize" "fusenapi/server/orders/internal/config" "github.com/golang-jwt/jwt" diff --git a/server/orders/internal/types/types.go b/server/orders/internal/types/types.go index 67d5d5aa..275b6c2c 100644 --- a/server/orders/internal/types/types.go +++ b/server/orders/internal/types/types.go @@ -6,13 +6,13 @@ import ( ) type GetOrderInvoiceReq struct { - Sn string `json:"sn"` - TimeZone string `json:"timeZone"` + Sn string `form:"sn"` + TimeZone int64 `form:"timeZone"` } type GetOrderInvoiceRsp struct { FileName string `json:"file_name"` - Pdf int64 `json:"pdf"` + Pdf string `json:"pdf"` } type Response struct { diff --git a/server_api/orders.api b/server_api/orders.api index 713118ac..3b4ff1c3 100644 --- a/server_api/orders.api +++ b/server_api/orders.api @@ -15,10 +15,10 @@ service orders { //获取订单发票 type GetOrderInvoiceReq { - Sn string `json:"sn"` - TimeZone string `json:"timeZone"` + Sn string `form:"sn"` + TimeZone int64 `form:"timeZone"` } type GetOrderInvoiceRsp { FileName string `json:"file_name"` - Pdf int64 `json:"pdf"` + Pdf string `json:"pdf"` } \ No newline at end of file diff --git a/utils/pdf/html_to_pdf.go b/utils/pdf/html_to_pdf.go index d2c0b7a8..f2a8b15e 100644 --- a/utils/pdf/html_to_pdf.go +++ b/utils/pdf/html_to_pdf.go @@ -6,7 +6,7 @@ import ( "strings" ) -// html转 Pdf dataType = 1 为网页url dataType = 2为网页内容 outFile为空则不保存 +// html转 Pdf dataType = 1 为网页url dataType = 2为网页内容 outFile为空则不保存(使用该方法需要安装工具 sudo apt-get install wkhtmltopdf) func HtmlToPdfBase64(content string, dataType int, outFile ...string) (string, error) { pdfg, err := wkhtmltopdf.NewPDFGenerator() if err != nil {