kobe6258 6 months ago
parent
commit
b7d4c76c36

+ 2 - 1
api/ht_account_facade.go

@@ -2,6 +2,7 @@ package api
 
 import (
 	"eta/eta_mini_ht_api/common/component/config"
+	"eta/eta_mini_ht_api/common/contants"
 	"eta/eta_mini_ht_api/common/utils/client"
 	"fmt"
 	"sync"
@@ -26,7 +27,7 @@ type HTAccountFacade struct {
 func (f *HTAccountFacade) GetInstance() *HTAccountFacade {
 	htFacadeOnce.Do(func() {
 		htFacade = &HTAccountFacade{
-			htConfig: config.GetConfig("HT").(*config.HTBizConfig),
+			htConfig: config.GetConfig(contants.HT).(*config.HTBizConfig),
 			client:   client.DefaultClient()}
 	})
 	return htFacade

+ 17 - 12
common/component/config/ht_biz_config.go

@@ -4,12 +4,13 @@ import "eta/eta_mini_ht_api/common/contants"
 
 // ESOpts es连接属性
 type HTOpts struct {
-	ReportIndex   string
-	MediaIndex    string
-	Encode        string
-	DesCode       string
-	Task          string
-	AccountApiUrl string
+	ReportIndex       string
+	MediaIndex        string
+	Encode            string
+	DesCode           string
+	Task              string
+	AccountApiUrl     string
+	WebhookPrivateKey string
 }
 type HTBizConfig struct {
 	BaseConfig
@@ -37,6 +38,9 @@ func (e *HTBizConfig) EnableTask() bool {
 	return false
 }
 
+func (e *HTBizConfig) GetWebhookPrivateKey() string {
+	return e.opts.WebhookPrivateKey
+}
 func (e *HTBizConfig) GetAccountApiUrl() string {
 	return e.opts.AccountApiUrl
 }
@@ -45,12 +49,13 @@ func (e *HTBizConfig) GetDesCode() string {
 }
 func (e *HTBizConfig) InitConfig() {
 	opts := HTOpts{
-		ReportIndex:   e.GetString("es_report_index"),
-		MediaIndex:    e.GetString("es_media_index"),
-		Encode:        e.GetString("response.encode"),
-		DesCode:       e.GetString("response.des_code"),
-		Task:          e.GetString("task"),
-		AccountApiUrl: e.GetString("api.account_url"),
+		ReportIndex:       e.GetString("es_report_index"),
+		MediaIndex:        e.GetString("es_media_index"),
+		Encode:            e.GetString("response.encode"),
+		DesCode:           e.GetString("response.des_code"),
+		Task:              e.GetString("task"),
+		AccountApiUrl:     e.GetString("api.account_url"),
+		WebhookPrivateKey: e.GetString("webhook.private_key"),
 	}
 	e.opts = opts
 }

+ 31 - 1
common/exception/exc_enums.go

@@ -5,6 +5,7 @@ import stringUtils "eta/eta_mini_ht_api/common/utils/string"
 type EtaError struct {
 	ErrorCode int
 	ErrorMsg  string
+	Exception string
 }
 
 func (e *EtaError) Error() string {
@@ -101,6 +102,16 @@ const (
 	GetAnalystMediaListFailed
 )
 
+const (
+	MerchantErrCode int = iota + 70000
+	ProductInfoError
+	IllegalProductId
+)
+const (
+	WebhookErrCode int = iota + 80000
+	SyncRiskError
+)
+
 // ErrorMap 用于存储错误码和错误信息的映射
 var ErrorMap = map[int]string{
 	UnknownError:          "未知错误",
@@ -168,6 +179,11 @@ var ErrorMap = map[int]string{
 	GetMediaListFailed:        "查询媒体列表失败",
 	GetAnalystMediaListFailed: "查询研究员媒体列表失败",
 	BindMobileFailed:          "绑定手机号失败",
+	//商户
+	ProductInfoError: "获取商品信息失败",
+	IllegalProductId: "非法的产品ID",
+	//webhook
+	SyncRiskError: "同步风险等级失败",
 }
 
 func Equals(code int, message string) bool {
@@ -186,7 +202,13 @@ func newException(code int, msg string) error {
 		ErrorMsg:  msg,
 	}
 }
-
+func newExceptionWithOrgMsg(code int, msg string, exception string) error {
+	return &EtaError{
+		ErrorCode: code,
+		ErrorMsg:  msg,
+		Exception: exception,
+	}
+}
 func New(code int) *EtaError {
 	err := ErrorMap[code]
 	if stringUtils.IsBlank(err) {
@@ -194,3 +216,11 @@ func New(code int) *EtaError {
 	}
 	return newException(code, err).(*EtaError)
 }
+
+func NewWithException(code int, exception string) *EtaError {
+	err := ErrorMap[code]
+	if stringUtils.IsBlank(err) {
+		return newException(UnknownError, ErrorMap[UnknownError]).(*EtaError)
+	}
+	return newExceptionWithOrgMsg(code, err, exception).(*EtaError)
+}

+ 15 - 0
common/utils/auth/rsa_utils.go

@@ -47,6 +47,21 @@ func ParsePrivateKeyFromPEM() (privateKey *rsa.PrivateKey, err error) {
 	return
 }
 
+func ParsePrivateKey(configPath string) (privateKey *rsa.PrivateKey, err error) {
+	pemBlock, err := os.ReadFile(configPath)
+	block, _ := pem.Decode(pemBlock)
+	if block == nil {
+		logger.Error("私钥解析失败")
+		return nil, errors.New("私钥解析失败")
+	}
+	privateInfo, err := x509.ParsePKCS8PrivateKey(block.Bytes)
+	if err != nil {
+		return nil, err
+	}
+	privateKey = privateInfo.(*rsa.PrivateKey)
+	return
+}
+
 // ParsePublicKeyFromPEM 解析RSA公钥
 func ParsePublicKeyFromPEM() (publicKey *rsa.PublicKey, err error) {
 	pemBlock, err := os.ReadFile("./conf/rsa_public_key.pem")

+ 110 - 0
common/utils/common_utils.go

@@ -0,0 +1,110 @@
+package utils
+
+import (
+	"fmt"
+	"strconv"
+	"time"
+)
+
+func IsValidOldIDCard(idCard string) bool {
+	if len(idCard) != 15 {
+		return false
+	}
+
+	// 分离出生日期
+	birthYearStr := idCard[6:8]
+	birthMonthStr := idCard[8:10]
+	birthDayStr := idCard[10:12]
+
+	// 转换为四位年份
+	birthYear, err := strconv.Atoi(birthYearStr)
+	if err != nil {
+		return false
+	}
+	if birthYear >= 0 && birthYear <= 99 {
+		birthYear = 1900 + birthYear
+	} else if birthYear >= 100 && birthYear <= 999 {
+		birthYear = 2000 + (birthYear % 100)
+	}
+
+	birthMonth, err := strconv.Atoi(birthMonthStr)
+	if err != nil {
+		return false
+	}
+
+	birthDay, err := strconv.Atoi(birthDayStr)
+	if err != nil {
+		return false
+	}
+
+	// 检查出生日期是否有效
+	if birthYear < 1900 || birthYear > time.Now().Year() ||
+		birthMonth < 1 || birthMonth > 12 ||
+		birthDay < 1 || birthDay > 31 {
+		return false
+	}
+
+	// 检查月份和天数的有效性
+	_, err = time.Parse(time.DateOnly, fmt.Sprintf("%d-%d-%d", birthYear, birthMonth, birthDay))
+	if err != nil {
+		return false
+	}
+
+	// 检查性别
+	genderCode, _ := strconv.Atoi(idCard[14:15])
+	if genderCode%2 == 0 {
+		return true // 女性
+	} else {
+		return true // 男性
+	}
+}
+func IsValidIDCard(idCard string) bool {
+	if len(idCard) != 18 {
+		return false
+	}
+
+	// 加权因子
+	weights := []int{7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2}
+	// 校验码映射表
+	checkSumMap := map[int]string{0: "1", 1: "0", 2: "X", 3: "9", 4: "8", 5: "7", 6: "6", 7: "5", 8: "4", 9: "3", 10: "2"}
+
+	// 计算校验码
+	sum := 0
+	for i, ch := range idCard[:17] {
+		digit, _ := strconv.Atoi(string(ch))
+		sum += digit * weights[i]
+	}
+	checkSum := sum % 11
+	checkDigit := checkSumMap[checkSum]
+
+	check := idCard[17:18]
+	// 检查最后一位是否匹配
+	if checkDigit != check {
+		return false
+	}
+
+	// 验证出生日期
+	birthYear, _ := strconv.Atoi(idCard[6:10])
+	birthMonth, _ := strconv.Atoi(idCard[10:12])
+	birthDay, _ := strconv.Atoi(idCard[12:14])
+
+	if birthYear < 1900 || birthYear > time.Now().Year() ||
+		birthMonth < 1 || birthMonth > 12 ||
+		birthDay < 1 || birthDay > 31 {
+		return false
+	}
+
+	// 检查月份和天数的有效性
+	_, err := time.Parse(time.DateOnly, fmt.Sprintf("%d-%d-%d", birthYear, birthMonth, birthDay))
+	if err != nil {
+		return false
+	}
+
+	// 检查性别
+	genderCode, _ := strconv.Atoi(idCard[16:17])
+	if genderCode%2 == 0 {
+		return true // 女性
+	} else {
+		return true // 男性
+	}
+}

+ 3 - 0
conf/rsa_private_key_ht.pem

@@ -0,0 +1,3 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCUtBFdP3BhGea1JwKoBbm1qfB5bV4BjBRZaD8Vu6FMmgqwPoKjA0FigGJFRSy+gcFc0A60MAFjsJKREKFL+Po9BL8d/vlmnkw7VqFidkMFRLINHQih7BgNt9ww0u9sl5CIQmd5eFixQAa1JPWtXefKAksQGLjkimytNQqW532m8onMkQZebaTs6q/s8Psf6tWeGt1Kc/9Kx6gGnHRoWHPZiXh0/AeYErJM3kIP/k4cKGFBZgmxhRDUOyOeFgU3R+IDs6+DOV/4l2GyZIwmMCUiWXWGIHnUBT/WrX9vXNby8gDncN3eKxWbvkIzHjdW3sVjduj4ZPDYlzAD1wYB0R15AgMBAAECggEAQPKk4uViYAbADT8MmfZmNkITFfX+qQ5VlPdDFmrs+FgPcwraY/h4Bl2sjjS4ZjymB7OXuHt/H9tvKIzq5hPIt/3qu18x7vAUu5YKxsaAXzyxx6h2rMJBfzxen7SBfhx0tA7MwvEdS817IIMUCrSJGGIIBu/FUYxkCwKvrVlOOCvsIcu7kr0tKV7KgL+fWb2y/68LORLykUMuFhyTcwAqorkCV7MPdvDtdcqtqArYNZn/lWUWaeKz5BqhG3nP0m2V1duVafRNpBmtFqQhwBsXCED3FR6QHNdyGYC4H7a0AYxXHd84HsYUTBugjw0i6XfvaHWUFXRIc/DY2SPxUOfNQQKBgQDI2znprBU1eHgXGLtFXwYcDjUf7BZtzL69RItQUsBbs2nDXZjQcNDZ+JjfqSj9XaRCjIQrfXiyWv+5x8m4E5MuvtyeCO2GqTCEo3mUDusvgR55sRArovq6YftPDmmSUD+V80H5GC9kxivR6BVlDjAO+mGrmKQG1uowQrT5OKO86wKBgQC9h2Ddv26dwni3K2iCJ6nMVMe8JredeYwkfG01sGR1IszPIO09XDMRFBdGwntyzBctTtAEtJxYHc54YNxxFyL0NDprb57G6ysuejq99KsRExBYymyOVwmHc2IKT6Vj5mDrE8A57xFp+1mouIYoFTa31oHhjOW1aqAmKHo/QXCmKwKBgAqInL/pe29DcyDa9i9MLXjZMeYLrp1xiGtKpfe/b0Ef5qMNTI9Z60oTJIlOSM0I1S18Sw4w1VydMx4eITEbLbPc5JsRIsvWIapDHIQsSB9EqUF+jLeNI5MUwmZB/j1jIgKOMF6M6ydg0Tl/72dOWCzg6rBiH/AP41ZGmVEcrFGjAoGAUttbarYk+s0pDxLoFnaWkeDCjSvz++FGdjD7YYxi7p6vISJI9RlYre+1mVaut2on+8PHxzbaXt9xA0l9NeeifVZT7+IVbOskrqX7Bk5vdwB5lgew262LWe8EfnOBX6I43qx8zhcI6udatBsvc1iK2mXp1BxP1DbwGp55iyvlvo8CgYEAoNj+YVQaRure36lhPI53Pm0HtJGPL9CAu944udW3kq1g3eL+FvuhMSuiCf94MrZjybYWGwflz0FcPzxrS6nmMNpVHsiG3lxRufPhKApKRpGGiI94MMkO0E94MxYSTnlDwjYSrRr3Fqe1YoFdQm5ZxXLZwl+NRMlu3RUNMzSM90A=
+-----END RSA PRIVATE KEY-----

+ 7 - 2
controllers/media/media_controller.go

@@ -131,6 +131,7 @@ func (m *MediaController) List(mediaType string, permissionIds string) {
 
 type RecordCountReq struct {
 	MediaId   int    `json:"MediaId"`
+	TraceId   string `json:"TraceId"`
 	MediaType string `json:"MediaType"`
 }
 
@@ -153,13 +154,16 @@ func (m *MediaController) Count() {
 		record := convertToRecordCount(recordReq)
 		record.UserId = userInfo.Id
 		record.Mobile = userInfo.Mobile
-		err = media.CountMedia(record)
+		traceId, err := media.CountMedia(record)
 		if err != nil {
 			err = exception.New(exception.MediaRecordClickCountFailed)
 			m.FailedResult("媒体点击记录失败", result)
 			return
 		}
-		m.SuccessResult("媒体点击记录成功", nil, result)
+		countResp := struct {
+			TraceId string `json:"traceId"`
+		}{TraceId: traceId}
+		m.SuccessResult("媒体点击记录成功", countResp, result)
 		return
 	})
 }
@@ -191,6 +195,7 @@ func (m *MediaController) GetMedia(mediaType string, mediaId int) {
 func convertToRecordCount(req *RecordCountReq) media.RecordCount {
 	return media.RecordCount{
 		MediaId:    req.MediaId,
+		TraceId:    req.TraceId,
 		MediaType:  req.MediaType,
 		Additional: "",
 	}

+ 43 - 0
controllers/order/order_controller.go

@@ -0,0 +1,43 @@
+package order
+
+import (
+	"eta/eta_mini_ht_api/common/exception"
+	"eta/eta_mini_ht_api/controllers"
+	productService "eta/eta_mini_ht_api/service/product"
+)
+
+type OrderController struct {
+	controllers.BaseController
+}
+
+type ProductOrderReq struct {
+	ProductId int `json:"productId"`
+}
+
+// CreateProductOrder  创建商品订单
+// @Summary 创建商品订单
+// @Description 创建商品订单
+// @Success 200 {object} controllers.BaseResponse
+// @router /createProductOrder [post]
+func (o *OrderController) CreateProductOrder() {
+	controllers.Wrap(&o.BaseController, func() (result *controllers.WrapData, err error) {
+		result = o.InitWrapData("获取商品详情失败")
+		err = exception.New(exception.IllegalProductId)
+		o.FailedResult("获取商品详情失败", result)
+		//客戶是否开户,是否有风险等级,是否风险等级过期
+		//报告的风险等级 套餐的风险等级
+		//创单
+
+		productInfo, err := productService.GetProductInfoById(productId)
+		if err != nil {
+			o.FailedResult("获取商品详情失败", result)
+			return
+		}
+		o.SuccessResult("获取商品详情成功", productInfo, result)
+		return
+	})
+	//
+	//获取用户信息
+	//userInfo := o.Data["user"].(user.User)
+
+}

+ 34 - 0
controllers/product/product_controller.go

@@ -0,0 +1,34 @@
+package product
+
+import (
+	"eta/eta_mini_ht_api/common/exception"
+	"eta/eta_mini_ht_api/controllers"
+	productService "eta/eta_mini_ht_api/service/product"
+)
+
+type ProductController struct {
+	controllers.BaseController
+}
+
+// GetProductInfo  获取商品信息
+// @Summary 获取商品信息
+// @Description 获取商品信息
+// @Success 200 {object} controllers.BaseResponse
+// @router /getProductInfo [get]
+func (p *ProductController) GetProductInfo(productId int) {
+	controllers.Wrap(&p.BaseController, func() (result *controllers.WrapData, err error) {
+		result = p.InitWrapData("获取商品详情失败")
+		if productId <= 0 {
+			err = exception.New(exception.IllegalProductId)
+			p.FailedResult("获取商品详情失败", result)
+			return
+		}
+		productInfo, err := productService.GetProductInfoById(productId)
+		if err != nil {
+			p.FailedResult("获取商品详情失败", result)
+			return
+		}
+		p.SuccessResult("获取商品详情成功", productInfo, result)
+		return
+	})
+}

+ 7 - 2
controllers/report/report_controller.go

@@ -301,6 +301,7 @@ func (r *ReportController) GetPermissions() {
 
 type RecordCountReq struct {
 	ReportId   int         `json:"reportId"`
+	TraceId    string      `json:"traceId"`
 	IpAddress  string      `json:"ipAddress"`
 	Location   string      `json:"location"`
 	Referer    string      `json:"referer"`
@@ -327,13 +328,16 @@ func (r *ReportController) Count() {
 		record := convertToRecordCount(recordReq)
 		record.UserId = userInfo.Id
 		record.Mobile = userInfo.Mobile
-		err = report.CountReport(record)
+		traceId, err := report.CountReport(record)
 		if err != nil {
 			r.FailedResult("研报点击记录失败", result)
 			err = exception.New(exception.ReportRecordClickCountFailed)
 			return
 		}
-		r.SuccessResult("研报点击记录成功", nil, result)
+		countResp := struct {
+			TraceId string `json:"traceId"`
+		}{TraceId: traceId}
+		r.SuccessResult("研报点击记录成功", countResp, result)
 		return
 	})
 }
@@ -364,6 +368,7 @@ func convertToRecordCount(req *RecordCountReq) report.RecordCount {
 	additionStr, _ := json.Marshal(req.Additional)
 	return report.RecordCount{
 		ReportId:   req.ReportId,
+		TraceId:    req.TraceId,
 		IpAddress:  req.IpAddress,
 		Location:   req.Location,
 		Referer:    req.Referer,

+ 46 - 13
controllers/web_hook/htfutures_account_controller.go

@@ -2,6 +2,7 @@ package web_hook
 
 import (
 	logger "eta/eta_mini_ht_api/common/component/log"
+	"eta/eta_mini_ht_api/common/exception"
 	"eta/eta_mini_ht_api/controllers"
 	userService "eta/eta_mini_ht_api/domian/user"
 )
@@ -10,6 +11,8 @@ type HTFuturesAccountController struct {
 	controllers.WebHookController
 }
 
+type Risk struct{}
+
 // SyncCustomerRiskLevel  风险测评同步接口
 // @Summary 风险测评同步接口
 // @Description 风险测评同步接口
@@ -20,31 +23,48 @@ func (h *HTFuturesAccountController) SyncCustomerRiskLevel() {
 		result = h.InitWrapData("同步风险等级")
 		syncCustomerRiskLevelReq := new(SyncCustomerRiskLevelReq)
 		h.GetPostParams(syncCustomerRiskLevelReq)
-		if syncCustomerRiskLevelReq.Name == "" {
+		custInfo := syncCustomerRiskLevelReq.CustInfo
+		riskInfo := syncCustomerRiskLevelReq.RiskInfo
+		if custInfo.ClientName == "" {
+			err = exception.New(exception.SyncRiskError)
 			h.FailedResult("用户名字不能为空", result)
 			return
 		}
-		if syncCustomerRiskLevelReq.PhoneNumber == "" {
+		if custInfo.MobileTel == "" {
+			err = exception.New(exception.SyncRiskError)
 			h.FailedResult("手机号码不能为空", result)
 			return
 		}
-		if syncCustomerRiskLevelReq.RiskLevel == "" {
+		if custInfo.IdNo == "" {
+			err = exception.New(exception.SyncRiskError)
+			h.FailedResult("身份证号不能为空", result)
+			return
+		}
+		//if !utils.IsValidIDCard(custInfo.IdNo) && !utils.IsValidOldIDCard(custInfo.IdNo) {
+		//	err = exception.New(exception.SyncRiskError)
+		//	h.FailedResult("身份证号不合法", result)
+		//	return
+		//}
+		if riskInfo.CorpRiskLevel == "" {
+			err = exception.New(exception.SyncRiskError)
 			h.FailedResult("风险等级不能为空", result)
 			return
 		}
-		if syncCustomerRiskLevelReq.RiskValidEndDate == "" {
-			h.FailedResult("风险测评有效期不能为空", result)
+		if riskInfo.CorpEndDate == "" {
+			err = exception.New(exception.SyncRiskError)
+			h.FailedResult("风险测评有效结束日期不能为空", result)
 			return
 		}
 		err = userService.UpdateRiskLevelInfo(userService.RiskLevelInfoDTO{
-			Name:             syncCustomerRiskLevelReq.Name,
-			PhoneNumber:      syncCustomerRiskLevelReq.PhoneNumber,
-			RiskLevel:        syncCustomerRiskLevelReq.RiskLevel,
-			RiskValidEndDate: syncCustomerRiskLevelReq.RiskValidEndDate,
+			Name:             custInfo.ClientName,
+			PhoneNumber:      custInfo.MobileTel,
+			RiskLevel:        riskInfo.CorpRiskLevel,
+			RiskValidEndDate: riskInfo.CorpEndDate,
 		})
 		if err != nil {
 			logger.ErrorWithTraceId(h.Ctx, err.Error())
 			h.FailedResult(err.Error(), result)
+			err = exception.New(exception.SyncRiskError)
 			return
 		}
 		logger.InfoWithTraceId(h.Ctx, err.Error())
@@ -55,8 +75,21 @@ func (h *HTFuturesAccountController) SyncCustomerRiskLevel() {
 }
 
 type SyncCustomerRiskLevelReq struct {
-	Name             string `json:"name"`
-	PhoneNumber      string `json:"phoneNumber"`
-	RiskLevel        string `json:"riskLevel"`
-	RiskValidEndDate string `json:"riskValidEndDate"`
+	CustInfo CustInfo `json:"custInfo"`
+	RiskInfo RiskInfo `json:"riskInfo"`
+}
+
+type CustInfo struct {
+	MobileTel  string `json:"mobile_tel"`
+	ClientName string `json:"client_name"`
+	IdKind     string `json:"id_kind"`
+	IdNo       string `json:"id_no"`
+}
+
+type RiskInfo struct {
+	CorpBeginDate  string `json:"corp_begin_date"`
+	CorpEndDate    string `json:"corp_end_date"`
+	UserInvestTerm string `json:"user_invest_term"`
+	UserInvestKind string `json:"user_invest_kind"`
+	CorpRiskLevel  string `json:"corp_risk_level"`
 }

+ 54 - 0
domian/merchant/merchant_product.go

@@ -0,0 +1,54 @@
+package merchant
+
+import (
+	"errors"
+	logger "eta/eta_mini_ht_api/common/component/log"
+	merchantDao "eta/eta_mini_ht_api/models/merchant"
+	"github.com/shopspring/decimal"
+	"gorm.io/gorm"
+)
+
+var (
+	typeTransfer = map[merchantDao.MerchantProductType]string{
+		merchantDao.Report:  "单篇报告",
+		merchantDao.Video:   "单个视频",
+		merchantDao.Audio:   "单个音频",
+		merchantDao.Package: "套餐",
+	}
+)
+
+type MerchantProductDTO struct {
+	Title       string
+	CoverSrc    string
+	Description string
+	Price       decimal.Decimal
+	RiskLevel   string
+	Type        string
+	IsPermanent bool
+	ValidDays   int
+}
+
+func GetMerchantProductById(id int) (product MerchantProductDTO, err error) {
+	productDao, err := merchantDao.GetMerchantProductById(id)
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			logger.Error("商品信息不存在[productId:%d]", id)
+		} else {
+			logger.Error("获取商品信息失败[productId:%d],err:", id, err)
+		}
+	}
+	product = convertToDTO(productDao)
+	return
+}
+func convertToDTO(product merchantDao.MerchantProduct) MerchantProductDTO {
+	return MerchantProductDTO{
+		Title:       product.Title,
+		CoverSrc:    product.CoverSrc,
+		Description: product.Description,
+		Price:       product.Price,
+		RiskLevel:   product.RiskLevel,
+		Type:        typeTransfer[product.Type],
+		IsPermanent: product.IsPermanent,
+		ValidDays:   product.ValidDays,
+	}
+}

+ 50 - 13
domian/user/user_source_click_flow_service.go

@@ -4,10 +4,13 @@ import (
 	"errors"
 	logger "eta/eta_mini_ht_api/common/component/log"
 	userDao "eta/eta_mini_ht_api/models/user"
+	"fmt"
+	"time"
 )
 
 type RecordCountDTO struct {
 	UserId     int
+	TraceId    string
 	Mobile     string
 	SourceId   int
 	SourceType userDao.SourceType
@@ -32,10 +35,27 @@ func getSourceType(mediaType string) userDao.SourceType {
 		return ""
 	}
 }
-
-func CountReport(record RecordCountDTO) (err error) {
+func generateTraceId(productType string) string {
+	return fmt.Sprintf("%s%d", productType, time.Now().UnixNano())
+}
+func CountReport(record RecordCountDTO) (traceId string, err error) {
 	dao := convertUserToReportFlow(record)
-	err = userDao.CountSourceClicks(dao)
+	if dao.TraceId == "" {
+		traceId = generateTraceId(string(dao.SourceType))
+		dao.TraceId = traceId
+		err = userDao.CountSourceClicks(dao)
+	} else {
+		var dbRecord userDao.UserSourceClickFlow
+		dbRecord, err = userDao.GetClickRecordByTraceId(dao.TraceId)
+		traceId = dbRecord.TraceId
+		if err != nil {
+			logger.Error("更新用户研报点击记录失败:%v", err)
+			return
+		}
+		currTime := time.Now()
+		dbRecord.ReadDurationSeconds = currTime.Sub(dbRecord.ClickTime).Milliseconds()
+		err = userDao.UpdateSourceClicks(dbRecord)
+	}
 	if err != nil {
 		logger.Error("插入用户研报点击记录失败:%v", err)
 		return
@@ -44,7 +64,7 @@ func CountReport(record RecordCountDTO) (err error) {
 	return
 }
 
-func CountMedia(record RecordCountDTO, mediaType string) (err error) {
+func CountMedia(record RecordCountDTO, mediaType string) (traceId string, err error) {
 	sourceType := getSourceType(mediaType)
 	if sourceType == "" {
 		logger.Error("媒体类型错误,%s", mediaType)
@@ -53,7 +73,22 @@ func CountMedia(record RecordCountDTO, mediaType string) (err error) {
 	}
 	record.SourceType = sourceType
 	dao := convertUserToMediaFlow(record)
-	err = userDao.CountSourceClicks(dao)
+	if dao.TraceId == "" {
+		traceId = generateTraceId(string(dao.SourceType))
+		dao.TraceId = traceId
+		err = userDao.CountSourceClicks(dao)
+	} else {
+		var dbRecord userDao.UserSourceClickFlow
+		dbRecord, err = userDao.GetClickRecordByTraceId(dao.TraceId)
+		traceId = dbRecord.TraceId
+		if err != nil {
+			logger.Error("更新用户研报点击记录失败:%v", err)
+			return
+		}
+		currTime := time.Now()
+		dbRecord.ReadDurationSeconds = currTime.Sub(dbRecord.ClickTime).Milliseconds()
+		err = userDao.UpdateSourceClicks(dbRecord)
+	}
 	if err != nil {
 		logger.Error("插入用户媒体点击记录失败:%v", err)
 		return
@@ -81,21 +116,23 @@ func GetHotReports(begin string, end string, limit int) (dtoList []HotReportDTO)
 	}
 	return
 }
-func convertUserToReportFlow(report RecordCountDTO) userDao.UserSourceClickFlow {
+func convertUserToReportFlow(record RecordCountDTO) userDao.UserSourceClickFlow {
 	return userDao.UserSourceClickFlow{
-		UserID:         report.UserId,
-		Mobile:         report.Mobile,
-		SourceId:       report.SourceId,
+		UserID:         record.UserId,
+		Mobile:         record.Mobile,
+		SourceId:       record.SourceId,
+		TraceId:        record.TraceId,
 		SourceType:     userDao.ReportSourceType,
-		IPAddress:      report.IpAddress,
-		Location:       report.Location,
-		Referer:        report.Referer,
-		AdditionalData: report.Additional,
+		IPAddress:      record.IpAddress,
+		Location:       record.Location,
+		Referer:        record.Referer,
+		AdditionalData: record.Additional,
 	}
 }
 func convertUserToMediaFlow(media RecordCountDTO) userDao.UserSourceClickFlow {
 	return userDao.UserSourceClickFlow{
 		UserID:     media.UserId,
+		TraceId:    media.TraceId,
 		Mobile:     media.Mobile,
 		SourceId:   media.SourceId,
 		SourceType: media.SourceType,

+ 1 - 0
go.mod

@@ -33,6 +33,7 @@ require (
 	github.com/prometheus/common v0.48.0 // indirect
 	github.com/prometheus/procfs v0.12.0 // indirect
 	github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
+	github.com/shopspring/decimal v1.4.0 // indirect
 	github.com/sirupsen/logrus v1.9.0 // indirect
 	github.com/spf13/cast v1.4.1 // indirect
 	github.com/tidwall/gjson v1.14.1 // indirect

+ 2 - 0
go.sum

@@ -104,6 +104,8 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
 github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
 github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 h1:DAYUYH5869yV94zvCES9F51oYtN5oGlwjxJJz7ZCnik=
 github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg=
+github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
+github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
 github.com/siddontang/go v0.0.0-20170517070808-cb568a3e5cc0 h1:QIF48X1cihydXibm+4wfAc0r/qyPyuFiPFRNphdMpEE=
 github.com/siddontang/go v0.0.0-20170517070808-cb568a3e5cc0/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw=
 github.com/silenceper/wechat/v2 v2.1.6 h1:2br2DxNzhksmvIBJ+PfMqjqsvoZmd/5BnMIfjKYUBgc=

+ 2 - 0
middleware/auth_middleware.go

@@ -76,6 +76,8 @@ var privateRoutes = []string{
 	"/analyst/mediaList",
 	"/media/count",
 	"/report/count",
+	"/product/*",
+	"/order/*",
 }
 
 func AuthMiddleware() web.FilterFunc {

+ 13 - 16
middleware/webhook_middleware.go

@@ -1,15 +1,20 @@
 package middleware
 
 import (
-	"encoding/base64"
 	"encoding/json"
+	"eta/eta_mini_ht_api/common/component/config"
 	logger "eta/eta_mini_ht_api/common/component/log"
+	"eta/eta_mini_ht_api/common/contants"
 	"eta/eta_mini_ht_api/common/utils/auth"
 	"fmt"
 	"github.com/beego/beego/v2/server/web"
 	"github.com/beego/beego/v2/server/web/context"
 )
 
+var (
+	htConfig = config.GetConfig(contants.HT).(*config.HTBizConfig)
+)
+
 func WebHookAuthMiddleware() web.FilterFunc {
 	return func(ctx *context.Context) {
 		body := ctx.Input.RequestBody
@@ -21,37 +26,29 @@ func WebHookAuthMiddleware() web.FilterFunc {
 			_ = ctx.JSONResp(rep)
 			return
 		}
-		privateKey, err := auth.ParsePrivateKeyFromPEM()
+
+		privateKey, err := auth.ParsePrivateKey(htConfig.GetWebhookPrivateKey())
 		if err != nil {
 			rep := webhookUnauthorized("系统异常")
 			logger.Error("解析私钥失败: %v", err)
 			_ = ctx.JSONResp(rep)
 			return
 		}
-		aesKey, err := auth.DecryptWithRSA(privateKey, webhookRequest.EncryptKey)
-		if err != nil {
-			rep := webhookUnauthorized("解析AES秘钥")
-			logger.Error("解析AES秘钥失败: %v", err)
-			_ = ctx.JSONResp(rep)
-			return
-		}
-		decodeKey, _ := base64.StdEncoding.DecodeString(string(aesKey))
 		logger.Info("解码请求: %v", webhookRequest.Data)
-		data, err := base64.StdEncoding.DecodeString(webhookRequest.Data)
-		aes, err := auth.DecryptWithAES(decodeKey, data)
+		decodeData, err := auth.DecryptWithRSA(privateKey, webhookRequest.Data)
 		if err != nil {
 			rep := webhookUnauthorized("解密请求体失败")
 			logger.Error("解密请求体失败: %v", err)
 			_ = ctx.JSONResp(rep)
 			return
 		}
-		fmt.Printf("解密后的请求: %v", string(aes))
-		ctx.Input.RequestBody = aes
+		fmt.Printf("解密后的请求: %v", string(decodeData))
+		ctx.Input.RequestBody = decodeData
 		return
 	}
 }
 
 type WebhookRequest struct {
-	Data       string `json:"data"`
-	EncryptKey string `json:"encryptKey"`
+	Data string `json:"data"`
+	//EncryptKey string `json:"encryptKey"`
 }

+ 1 - 1
models/financial_analyst/financial_analyst.go

@@ -61,7 +61,7 @@ func GetAnalystByName(name string) (analyst CrmFinancialAnalyst, err error) {
 
 func GetCount() (total int64, latestId int64) {
 	db := models.Main()
-	err := db.Model(&CrmFinancialAnalyst{}).Select("count(*) count").Scan(&total).Order("id asc").Error
+	err := db.Model(&CrmFinancialAnalyst{}).Select("count(*) count").Order("id asc").Scan(&total).Error
 	if err != nil {
 		logger.Error("查询研究员列表失败,%v", err)
 		return 0, 0

+ 18 - 0
models/merchant/merchant.go

@@ -0,0 +1,18 @@
+package merchant
+
+import "time"
+
+// Merchant 商户信息结构体
+type Merchant struct {
+	ID           int       `gorm:"column:id;primary_key;comment:商户ID"`
+	MerchantID   string    `gorm:"column:merchant_id;type:varchar(255);comment:外部商户ID"`
+	MerchantName string    `gorm:"column:merchant_name;type:varchar(255);comment:商户名称"`
+	Enabled      bool      `gorm:"column:enabled;type:tinyint(1);comment:是否启用"`
+	CreatedTime  time.Time `gorm:"column:created_time;type:datetime;comment:创建时间"`
+	UpdatedTime  time.Time `gorm:"column:updated_time;type:datetime;comment:更新时间"`
+}
+
+// TableName 指定表名
+func (Merchant) TableName() string {
+	return "merchants"
+}

+ 20 - 0
models/merchant/merchant_package.go

@@ -0,0 +1,20 @@
+package merchant
+
+import (
+	"time"
+)
+
+// MerchantPackage 商户套餐信息结构体
+type MerchantPackage struct {
+	ID              int       `gorm:"column:id;primary_key;autoIncrement;comment:主键"`
+	ReportSourceIds string    `gorm:"column:report_source_ids;type:varchar(255);comment:报告来源"`
+	VideoSourceIds  string    `gorm:"column:video_source_ids;type:varchar(255);comment:视频来源"`
+	AudioSourceIds  string    `gorm:"column:audio_source_ids;type:varchar(255);comment:音频来源"`
+	CreatedTime     time.Time `gorm:"column:created_time;type:datetime;comment:创建时间"`
+	UpdatedTime     time.Time `gorm:"column:updated_time;type:datetime;comment:更新时间"`
+}
+
+// TableName 指定表名
+func (MerchantPackage) TableName() string {
+	return "merchant_packages"
+}

+ 60 - 0
models/merchant/merchant_product.go

@@ -0,0 +1,60 @@
+package merchant
+
+import (
+	"eta/eta_mini_ht_api/models"
+	"github.com/shopspring/decimal"
+	"gorm.io/gorm"
+	"time"
+)
+
+type SaleStatus string
+type MerchantProductType string
+
+const (
+	detailColumns = "title,price,is_permanent,valid_days,type"
+)
+const (
+	OnSale  SaleStatus          = "on_sale"  //上架
+	OffSale SaleStatus          = "off_sale" //下架
+	Package MerchantProductType = "package"
+	Report  MerchantProductType = "report"
+	Video   MerchantProductType = "video"
+	Audio   MerchantProductType = "audio"
+)
+
+// MerchantProduct 商户产品信息结构体
+type MerchantProduct struct {
+	ID          int                 `gorm:"column:id;primary_key;autoIncrement;comment:主键"`
+	SourceID    int                 `gorm:"column:source_id;type:int(11);comment:单品或者套餐对应的主键"`
+	Title       string              `gorm:"column:title;type:varchar(255);comment:标题"`
+	CoverSrc    string              `gorm:"column:cover_src;type:varchar(255);comment:封面图片"`
+	Description string              `gorm:"column:description;type:varchar(255);comment:描述"`
+	Price       decimal.Decimal     `gorm:"column:price;type:decimal(10,2);comment:价格"`
+	RiskLevel   string              `gorm:"column:risk_level;type:varchar(255);not null;comment:风险等级"`
+	Type        MerchantProductType `gorm:"column:type;type:enum('report','video','audio','package');not null;comment:类型"`
+	IsPermanent bool                `gorm:"column:is_permanent;type:int(1);not null;default:0;comment:是否永久"`
+	ValidDays   int                 `gorm:"column:valid_days;type:int(11);comment:有效期天数"`
+	SaleStatus  SaleStatus          `gorm:"column:sale_status;type:enum('on_sale','off_sale');not null;default:'on_sale';comment:上架/下架状态"`
+	Deleted     bool                `gorm:"column:deleted;type:tinyint(1);not null;default:0;comment:是否删除"`
+	CreatedTime time.Time           `gorm:"column:created_time;type:datetime;comment:创建时间"`
+	UpdatedTime time.Time           `gorm:"column:updated_time;type:datetime;comment:更新时间;default:CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP"`
+}
+
+// TableName 指定表名
+func (MerchantProduct) TableName() string {
+	return "merchant_products"
+}
+
+func (m *MerchantProduct) BeforeCreate(_ *gorm.DB) (err error) {
+	m.CreatedTime = time.Now()
+	m.Deleted = false
+	m.SaleStatus = OffSale
+	return
+}
+
+func GetMerchantProductById(id int) (product MerchantProduct, err error) {
+	db := models.Main()
+	err = db.Select(detailColumns).Where("id = ? and deleted =?", id, false).First(&product).Error
+
+	return
+}

+ 26 - 17
models/user/template_user.go

@@ -11,24 +11,32 @@ const (
 	Deleted    = 1
 )
 
+type AccountStatus string
+
+const (
+	Open   AccountStatus = "open"
+	Unopen AccountStatus = "unOpen"
+)
+
 type TemplateUser struct {
-	Id               int       `gorm:"column:id;primaryKey;autoIncrement:'id'"`
-	Username         string    `gorm:"column:username;type:varchar(20);comment:用户名"`
-	Mobile           string    `gorm:"column:mobile;type:varchar(15);comment:手机号"`
-	OpenId           string    `gorm:"column:open_id;type:varchar(50);comment:open_id"`
-	GzhOpenId        string    `gorm:"column:gzh_open_id;type:varchar(255);comment:gzh_open_id"`
-	UnionId          string    `gorm:"column:union_id;type:varchar(50);comment:union_id"`
-	ReadCount        int       `gorm:"column:read_count;type:int(11);comment:阅读次数"`
-	FollowingGzh     int       `gorm:"column:following_gzh;type:int(1);comment:是否关注公众号"`
-	LastReadTime     time.Time `gorm:"column:last_read_time;type:timestamps;comment:最后阅读时间"`
-	LastLoginTime    time.Time `gorm:"column:last_login_time;type:timestamps;comment:最后登录时间"`
-	LastLogoutTime   time.Time `gorm:"column:last_logout_time;type:timestamps;comment:最后登出时间"`
-	LoginStatus      LogType   `gorm:"column:login_status;type:enum('login','logout');comment:登录"`
-	RiskLevel        string    `gorm:"column:risk_level;type:varchar(255);comment:风险等级"`
-	RiskValidEndDate string    `gorm:"column:risk_valid_end_date;type:varchar(20);comment:风险等级有效期"`
-	IsDeleted        int       `gorm:"column:is_deleted;type:int(11);comment:是否删除"`
-	CreatedTime      time.Time `gorm:"column:created_time;type:timestamps;comment:创建时间"`
-	UpdatedTime      time.Time `gorm:"column:updated_time;type:timestamps;comment:更新时间"`
+	Id               int           `gorm:"column:id;primaryKey;autoIncrement:'id'"`
+	Username         string        `gorm:"column:username;type:varchar(20);comment:用户名"`
+	Mobile           string        `gorm:"column:mobile;type:varchar(15);comment:手机号"`
+	OpenId           string        `gorm:"column:open_id;type:varchar(50);comment:open_id"`
+	GzhOpenId        string        `gorm:"column:gzh_open_id;type:varchar(255);comment:gzh_open_id"`
+	UnionId          string        `gorm:"column:union_id;type:varchar(50);comment:union_id"`
+	ReadCount        int           `gorm:"column:read_count;type:int(11);comment:阅读次数"`
+	FollowingGzh     int           `gorm:"column:following_gzh;type:int(1);comment:是否关注公众号"`
+	LastReadTime     time.Time     `gorm:"column:last_read_time;type:timestamps;comment:最后阅读时间"`
+	LastLoginTime    time.Time     `gorm:"column:last_login_time;type:timestamps;comment:最后登录时间"`
+	LastLogoutTime   time.Time     `gorm:"column:last_logout_time;type:timestamps;comment:最后登出时间"`
+	LoginStatus      LogType       `gorm:"column:login_status;type:enum('login','logout');comment:登录"`
+	AccountStatus    AccountStatus `gorm:"column:account_status;type:enum('normal','risk');comment:账号状态"`
+	RiskLevel        string        `gorm:"column:risk_level;type:varchar(255);comment:风险等级"`
+	RiskValidEndDate string        `gorm:"column:risk_valid_end_date;type:varchar(20);comment:风险等级有效期"`
+	IsDeleted        int           `gorm:"column:is_deleted;type:int(11);comment:是否删除"`
+	CreatedTime      time.Time     `gorm:"column:created_time;type:timestamps;comment:创建时间"`
+	UpdatedTime      time.Time     `gorm:"column:updated_time;type:timestamps;comment:更新时间"`
 }
 
 func UpdateRiskLevelInfo(user TemplateUser) (err error) {
@@ -39,6 +47,7 @@ func (t *TemplateUser) BeforeCreate(tx *gorm.DB) (err error) {
 	t.CreatedTime = time.Now()
 	t.LoginStatus = Logout
 	t.IsDeleted = NotDeleted
+	t.AccountStatus = Unopen
 	return
 }
 

+ 23 - 12
models/user/user_source_click_flow.go

@@ -12,29 +12,40 @@ const (
 
 // UserReportClickFlow 用户点击研报流水记录
 type UserSourceClickFlow struct {
-	ID             int        `gorm:"column:id;primaryKey;autoIncrement:'id'"`
-	UserID         int        `gorm:"column:user_id"` // 用户ID\
-	Mobile         string     `gorm:"column:mobile"`
-	SourceId       int        `gorm:"column:source_id"` // 研报ID
-	SourceType     SourceType `gorm:"column:source_type;type:enum('REPORT','VIDEO','AUDIO')"`
-	ClickTime      time.Time  `gorm:"column:click_time"` // 点击时间
-	IPAddress      string     `gorm:"column:ip_address"` // IP地址
-	Location       string     `gorm:"column:location"`   // 地理位置
-	Hidden         bool       `gorm:"column:hidden;type:tinyint(1);default:0"`
-	Referer        string     `gorm:"column:referer"`         // 来源页面
-	AdditionalData string     `gorm:"column:additional_data"` // 额外数据
+	ID                  int        `gorm:"column:id;primaryKey;autoIncrement:'id'"`
+	TraceId             string     `gorm:"column:trace_id"`
+	UserID              int        `gorm:"column:user_id"` // 用户ID\
+	Mobile              string     `gorm:"column:mobile"`
+	SourceId            int        `gorm:"column:source_id"` // 研报ID
+	SourceType          SourceType `gorm:"column:source_type;type:enum('REPORT','VIDEO','AUDIO')"`
+	ClickTime           time.Time  `gorm:"column:click_time"` // 点击时间
+	ReadDurationSeconds int64      `gorm:"column:read_duration_seconds"`
+	IPAddress           string     `gorm:"column:ip_address"` // IP地址
+	Location            string     `gorm:"column:location"`   // 地理位置
+	Hidden              bool       `gorm:"column:hidden;type:tinyint(1);default:0"`
+	Referer             string     `gorm:"column:referer"`         // 来源页面
+	AdditionalData      string     `gorm:"column:additional_data"` // 额外数据
 }
 
 func (v *UserSourceClickFlow) BeforeCreate(tx *gorm.DB) (err error) {
 	v.ClickTime = time.Now()
 	return
 }
-
+func UpdateSourceClicks(count UserSourceClickFlow) (err error) {
+	db := models.Main()
+	err = db.Table(userSourceClickFlows).Where("trace_id =?", count.TraceId).UpdateColumn("read_duration_seconds", count.ReadDurationSeconds).Error
+	return
+}
 func CountSourceClicks(count UserSourceClickFlow) (err error) {
 	db := models.Main()
 	err = db.Create(&count).Error
 	return
 }
+func GetClickRecordByTraceId(traceId string) (record UserSourceClickFlow, err error) {
+	db := models.Main()
+	err = db.Select("*").Where("trace_id=?", traceId).First(&record).Error
+	return
+}
 
 type CountClickFlowById struct {
 	SourceId int `gorm:"column:source_id"`

+ 20 - 0
routers/commentsRouter.go

@@ -52,6 +52,26 @@ func init() {
             Filters: nil,
             Params: nil})
 
+    beego.GlobalControllerRouter["eta/eta_mini_ht_api/controllers/order:OrderController"] = append(beego.GlobalControllerRouter["eta/eta_mini_ht_api/controllers/order:OrderController"],
+        beego.ControllerComments{
+            Method: "CreateProductOrder",
+            Router: `/createProductOrder`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_mini_ht_api/controllers/product:ProductController"] = append(beego.GlobalControllerRouter["eta/eta_mini_ht_api/controllers/product:ProductController"],
+        beego.ControllerComments{
+            Method: "GetProductInfo",
+            Router: `/getProductInfo`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(
+				param.New("productId"),
+			),
+            Filters: nil,
+            Params: nil})
+
     beego.GlobalControllerRouter["eta/eta_mini_ht_api/controllers/report:ReportController"] = append(beego.GlobalControllerRouter["eta/eta_mini_ht_api/controllers/report:ReportController"],
         beego.ControllerComments{
             Method: "Count",

+ 12 - 0
routers/router.go

@@ -2,6 +2,8 @@ package routers
 
 import (
 	"eta/eta_mini_ht_api/controllers/media"
+	"eta/eta_mini_ht_api/controllers/order"
+	"eta/eta_mini_ht_api/controllers/product"
 	"eta/eta_mini_ht_api/controllers/report"
 	"eta/eta_mini_ht_api/controllers/user"
 	"eta/eta_mini_ht_api/controllers/web_hook"
@@ -55,6 +57,16 @@ func init() {
 				&web_hook.HTFuturesAccountController{},
 			),
 		),
+		web.NSNamespace("/product",
+			web.NSInclude(
+				&product.ProductController{},
+			),
+		),
+		web.NSNamespace("/order",
+			web.NSInclude(
+				&order.OrderController{},
+			),
+		),
 	)
 	web.AddNamespace(ns)
 }

+ 3 - 1
service/media/media_service.go

@@ -28,6 +28,7 @@ type RecordCount struct {
 	UserId     int
 	Mobile     string
 	MediaId    int
+	TraceId    string
 	MediaType  string
 	IpAddress  string
 	Location   string
@@ -38,12 +39,13 @@ type RecordCount struct {
 func convertToMediaCountDTO(record RecordCount) (dto userService.RecordCountDTO) {
 	return userService.RecordCountDTO{
 		UserId:   record.UserId,
+		TraceId:  record.TraceId,
 		Mobile:   record.Mobile,
 		SourceId: record.MediaId,
 	}
 }
 
-func CountMedia(count RecordCount) (err error) {
+func CountMedia(count RecordCount) (traceId string, err error) {
 	meida, err := mediaService.GetMediaById(count.MediaType, count.MediaId)
 	if err != nil {
 		err = exception.New(exception.MediaFoundFailed)

+ 42 - 0
service/product/product_service.go

@@ -0,0 +1,42 @@
+package product
+
+import (
+	"eta/eta_mini_ht_api/common/exception"
+	merchantService "eta/eta_mini_ht_api/domian/merchant"
+	"github.com/shopspring/decimal"
+	"time"
+)
+
+type ProductDTO struct {
+	Title       string
+	Description string
+	Price       decimal.Decimal
+	CoverSrc    string
+	Type        string
+	BeginDate   string
+	EndDate     string
+}
+
+func GetProductInfoById(productId int) (product ProductDTO, err error) {
+	merchantProduct, err := merchantService.GetMerchantProductById(productId)
+	if err != nil {
+		err = exception.NewWithException(exception.ProductInfoError, err.Error())
+		return
+	}
+	product = convertToProductDTO(merchantProduct)
+	return
+}
+func convertToProductDTO(product merchantService.MerchantProductDTO) (productDTO ProductDTO) {
+	beginDate := time.Now()
+	endDate := beginDate.Add(time.Duration(product.ValidDays) * 24 * time.Hour)
+	productDTO = ProductDTO{
+		Title:       product.Title,
+		Description: product.Description,
+		Price:       product.Price,
+		CoverSrc:    product.CoverSrc,
+		Type:        product.Type,
+		BeginDate:   beginDate.Format(time.DateOnly),
+		EndDate:     endDate.Format(time.DateOnly),
+	}
+	return
+}

+ 3 - 1
service/report/report_service.go

@@ -63,6 +63,7 @@ type HotRankedReport struct {
 
 type RecordCount struct {
 	UserId     int
+	TraceId    string
 	Mobile     string
 	ReportId   int
 	IpAddress  string
@@ -506,7 +507,7 @@ func GetReportPageByAnalyst(pageInfo page.PageInfo, analyst string, reportIds []
 	}
 	return
 }
-func CountReport(count RecordCount) error {
+func CountReport(count RecordCount) (traceId string, err error) {
 	dto := convertToRecordCountDTO(count)
 	return userService.CountReport(dto)
 }
@@ -681,6 +682,7 @@ func convertToPublishRankedReportList(dtoList []reportService.ReportDTO) (report
 func convertToRecordCountDTO(record RecordCount) (dto userService.RecordCountDTO) {
 	return userService.RecordCountDTO{
 		UserId:     record.UserId,
+		TraceId:    record.TraceId,
 		Mobile:     record.Mobile,
 		SourceId:   record.ReportId,
 		IpAddress:  record.IpAddress,