浏览代码

创建商品订单

kobe6258 5 月之前
父节点
当前提交
a0295ab6f2

+ 185 - 0
api/ht_account_api.go

@@ -0,0 +1,185 @@
+package api
+
+import (
+	"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/exception"
+	"eta/eta_mini_ht_api/common/utils/auth"
+	"eta/eta_mini_ht_api/common/utils/client"
+	"fmt"
+	"io"
+	"sync"
+)
+
+const (
+	clientSuitInfoUrl = "/open/api/getClientSuitInfo"
+	accessTokenUrl    = "/api/openapi/authless/token"
+)
+
+var (
+	htFacadeOnce sync.Once
+
+	htFacade *HTAccountApi
+)
+
+type HTAccountApi struct {
+	htConfig *config.HTBizConfig
+	// HTTP请求客户端
+	client *client.HttpClient
+}
+
+func GetInstance() *HTAccountApi {
+	htFacadeOnce.Do(func() {
+		htFacade = &HTAccountApi{
+			htConfig: config.GetConfig(contants.HT).(*config.HTBizConfig),
+			client:   client.DefaultClient()}
+	})
+	return htFacade
+}
+
+type ClientSuitInfoReq struct {
+	ClientName string `json:"client_name"`
+	IdKind     int    `json:"id_kind"`
+	IdNo       string `json:"id_no"`
+}
+
+func (f *HTAccountApi) GetCustomerRiskLevelInfo(req ClientSuitInfoReq) (info CustomerAccountInfo, err error) {
+	url := f.htConfig.GetAccountApiUrl() + clientSuitInfoUrl
+	resp, err := f.client.Post(url, nil)
+	if err != nil {
+		logger.Error("调用CAP customerRiskInfo接口失败:[%v]", err)
+		return
+	}
+	defer func(Body io.ReadCloser) {
+		closeErr := Body.Close()
+		if closeErr != nil {
+			logger.Error("关闭Response失败:%v", closeErr)
+		}
+	}(resp.Body)
+	body, _ := io.ReadAll(resp.Body)
+	return decodeResponse(body)
+}
+
+type AccessTokenReq struct {
+	AppId     string `json:"app_id"`
+	SecretKey string `json:"secret_key"`
+}
+
+func (f *HTAccountApi) GetAccessToken() (token TokenInfo, err error) {
+	url := f.htConfig.GetAccountApiUrl() + accessTokenUrl
+	req := AccessTokenReq{
+		AppId:     f.htConfig.GetAppId(),
+		SecretKey: f.htConfig.GetSecretKey(),
+	}
+	resp, err := f.client.Post(url, req)
+	if err != nil {
+		logger.Error("调用CAP accessToken接口失败:[%v]", err)
+		return
+	}
+	defer func(Body io.ReadCloser) {
+		closeErr := Body.Close()
+		if closeErr != nil {
+			logger.Error("关闭Response失败:%v", closeErr)
+		}
+	}(resp.Body)
+	body, _ := io.ReadAll(resp.Body)
+
+	var tokenResp AccessTokenResp
+	err = json.Unmarshal(body, &tokenResp)
+	if err != nil {
+		logger.Warn("[cap 接口调用]解析cap token接口应答失败:%v", err)
+		err = exception.New(exception.GetCapTokenFailed)
+		return
+	}
+	if tokenResp.Error.ErrorNo != "0" {
+		logger.Warn("[cap 接口调用] 获取token失败:[code:%v, msg:%v, path:%v]", tokenResp.Error.ErrorNo, tokenResp.Error.ErrorInfo, tokenResp.Error.ErrorPathInfo)
+		err = exception.NewWithException(exception.GetCapTokenFailed, tokenResp.Error.ErrorInfo)
+		return
+	}
+
+	token = TokenInfo{
+		AccessToken: tokenResp.Data.AccessToken,
+		ExpiresAt:   tokenResp.Data.ExpiresAt,
+	}
+	return
+}
+
+type AccessTokenResp struct {
+	Error Error           `json:"error"`
+	Data  AccessTokenData `json:"data"`
+}
+
+type Error struct {
+	ErrorNo       string `json:"error_no"`
+	ErrorInfo     string `json:"error_info"`
+	ErrorPathInfo string `json:"error_path_info"`
+}
+
+type AccessTokenData struct {
+	AccessToken string `json:"accessToken"`
+	ExpiresAt   string `json:"expiresAt"`
+}
+
+type TokenInfo struct {
+	AccessToken string
+	ExpiresAt   string
+}
+
+type WebhookRequest struct {
+	Data string `json:"data"`
+}
+
+type SyncCustomerRiskLevelReq struct {
+	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"`
+	IdBeginDate string `json:"id_begindate"`
+	IdEndDate   string `json:"id_enddate"`
+}
+
+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"`
+}
+type CustomerAccountInfo struct {
+	CustInfo CustInfo `json:"custInfo"`
+	RiskInfo RiskInfo `json:"riskInfo"`
+}
+
+func decodeResponse(resp []byte) (info CustomerAccountInfo, err error) {
+	webhookRequest := new(WebhookRequest)
+	err = json.Unmarshal(resp, &webhookRequest)
+	if err != nil {
+		logger.Error("WebhookRequest解析失败: %v", err)
+		return
+	}
+	privateKey, err := auth.ParsePrivateKey(htFacade.htConfig.GetWebhookPrivateKey())
+	if err != nil {
+		logger.Error("解析私钥失败: %v", err)
+		return
+	}
+	logger.Info("解码请求: %v", webhookRequest.Data)
+	decodeData, err := auth.DecryptWithRSA(privateKey, webhookRequest.Data)
+	if err != nil {
+		logger.Error("解密请求体失败: %v", err)
+		return
+	}
+	fmt.Printf("解密后的请求: %v", string(decodeData))
+	err = json.Unmarshal(decodeData, &info)
+	if err != nil {
+		logger.Error("customerInfo解析失败: %v", err)
+		return
+	}
+	return
+}

+ 0 - 44
api/ht_account_facade.go

@@ -1,44 +0,0 @@
-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"
-)
-
-const (
-	clientSuitInfoUrl = "getClientSuitInfo"
-)
-
-var (
-	htFacadeOnce sync.Once
-
-	htFacade *HTAccountFacade
-)
-
-type HTAccountFacade struct {
-	htConfig *config.HTBizConfig
-	// HTTP请求客户端
-	client *client.HttpClient
-}
-
-func (f *HTAccountFacade) GetInstance() *HTAccountFacade {
-	htFacadeOnce.Do(func() {
-		htFacade = &HTAccountFacade{
-			htConfig: config.GetConfig(contants.HT).(*config.HTBizConfig),
-			client:   client.DefaultClient()}
-	})
-	return htFacade
-}
-
-func (f *HTAccountFacade) GetCustomerRiskLevelInfo() string {
-	url := f.htConfig.GetAccountApiUrl() + clientSuitInfoUrl
-	resp, err := f.client.Post(url, nil)
-	if err != nil {
-		return ""
-	}
-	fmt.Sprintln(resp)
-	return ""
-}

+ 11 - 0
common/component/config/ht_biz_config.go

@@ -12,6 +12,8 @@ type HTOpts struct {
 	AccountApiUrl     string
 	WebhookPrivateKey string
 	AccountInfoUrl    string
+	CapAppId          string
+	CapSecretKey      string
 }
 type HTBizConfig struct {
 	BaseConfig
@@ -48,6 +50,13 @@ func (e *HTBizConfig) GetWebhookPrivateKey() string {
 func (e *HTBizConfig) GetAccountApiUrl() string {
 	return e.opts.AccountApiUrl
 }
+
+func (e *HTBizConfig) GetAppId() string {
+	return e.opts.CapAppId
+}
+func (e *HTBizConfig) GetSecretKey() string {
+	return e.opts.CapSecretKey
+}
 func (e *HTBizConfig) GetDesCode() string {
 	return e.opts.DesCode
 }
@@ -61,6 +70,8 @@ func (e *HTBizConfig) InitConfig() {
 		AccountApiUrl:     e.GetString("api.account_url"),
 		WebhookPrivateKey: e.GetString("webhook.private_key"),
 		AccountInfoUrl:    e.GetString("api.account_url"),
+		CapAppId:          e.GetString("api.app_id"),
+		CapSecretKey:      e.GetString("api.secret_key"),
 	}
 	e.opts = opts
 }

+ 19 - 1
common/exception/exc_enums.go

@@ -18,6 +18,7 @@ const (
 	UnknownError
 	Unauthorized
 	SysError
+	QueryRiskMappingError
 )
 
 // BIZErrCode  业务错误
@@ -44,6 +45,7 @@ const (
 	UserErrCode int = iota + 30000 // iota 自动递增,从 1 开始
 	TemplateUserNotFound
 	TemplateUserFoundFailed
+	IllegalTemplateUserId
 	TemplateUserCreateFailed
 	TemplateUserBindFailed
 	GenerateTokenFailed
@@ -70,6 +72,12 @@ const (
 	CheckFollowStatusByNamesFailed
 	RiskUnTestError
 	RiskExpiredError
+	RiskUnMatchError
+	OfficialUserFoundError
+	OfficialUserNotFound
+	SubscribeFailed
+	GenerateOrderNoFailed
+	IllegalOrderNo
 )
 
 // WechatErrCode 微信
@@ -111,6 +119,7 @@ const (
 const (
 	WebhookErrCode int = iota + 80000
 	SyncRiskError
+	GetCapTokenFailed
 )
 
 // ErrorMap 用于存储错误码和错误信息的映射
@@ -118,6 +127,7 @@ var ErrorMap = map[int]string{
 	SysError:              "系统异常",
 	UnknownError:          "未知错误",
 	Unauthorized:          "用户未授权",
+	QueryRiskMappingError: "查询风险等级映射设置错误",
 	IllegalCodeLength:     "无效的验证码位数设置",
 	IllegalPhoneNumber:    "无效的手机号码",
 	SMSCodeGenerateFailed: "生成手机验证码失败",
@@ -134,6 +144,7 @@ var ErrorMap = map[int]string{
 	TemplateUserNotFound:               "临时用户记录不存在",
 	LogoutFailed:                       "退出登录失败",
 	TemplateUserFoundFailed:            "查询临时用户表失败",
+	IllegalTemplateUserId:              "不合法的临时用户ID",
 	TemplateUserCreateFailed:           "创建临时用户失败",
 	TemplateUserBindFailed:             "临时用户绑定小程序失败",
 	GenerateTokenFailed:                "创建token失败",
@@ -158,6 +169,12 @@ var ErrorMap = map[int]string{
 	BatchFollowingAnalystFailed:        "批量关注研究员列表失败",
 	RiskUnTestError:                    "客户未做风险测评",
 	RiskExpiredError:                   "客户风险测评已过期",
+	RiskUnMatchError:                   "客户风险测评不匹配",
+	OfficialUserFoundError:             "获取正式用户信息失败",
+	OfficialUserNotFound:               "用户未开户",
+	SubscribeFailed:                    "订阅失败",
+	GenerateOrderNoFailed:              "生成订单号",
+	IllegalOrderNo:                     "非法的产品订单号",
 	//微信
 	WeChatServerError:    "微信服务器发生错误",
 	WechatUserInfoFailed: "获取微信用户信息失败",
@@ -185,7 +202,8 @@ var ErrorMap = map[int]string{
 	ProductInfoError: "获取商品信息失败",
 	IllegalProductId: "非法的产品ID",
 	//webhook
-	SyncRiskError: "同步风险等级失败",
+	SyncRiskError:     "同步风险等级失败",
+	GetCapTokenFailed: "获取cap token失败",
 }
 
 func Equals(code int, message string) bool {

+ 16 - 0
common/utils/client/http_client.go

@@ -77,10 +77,26 @@ func (hc *HttpClient) Post(url string, data interface{}) (resp *http.Response, e
 	}
 	body := io.NopCloser(strings.NewReader(string(dataStr)))
 	req, err := http.NewRequest(http.MethodPost, url, body)
+	req.Header.Set("Content-Type", "application/json")
 	if err != nil {
 		logger.Error("创建POST请求失败: %v", err)
 	}
 	resp, err = hc.DoWithRetry(req.Context(), req)
+	code := resp.StatusCode
+	if code != 200 {
+		logger.Error("请求错误应答,状态码:%d", code)
+		errMsg := fmt.Sprintf("请求状态码异常,StatusCode:[%d]", code)
+		respBody, respErr := io.ReadAll(resp.Body)
+		if respErr != nil {
+			logger.Error("读取body失败,err:%v", err)
+			err = errors.New(errMsg)
+			return
+		}
+		logger.Error("请求错误应答,body:%s", string(respBody))
+		errMsg = fmt.Sprintf("%s,body:%s", errMsg, string(respBody))
+		err = errors.New(errMsg)
+		return
+	}
 	return
 }
 

+ 6 - 2
common/utils/redis/key_generator.go

@@ -3,8 +3,9 @@ package redis
 import "fmt"
 
 const (
-	SmsKeyPrefix     = "sms_code:"
-	LoginTokenPrefix = "login:token:"
+	SmsKeyPrefix      = "sms_code:"
+	LoginTokenPrefix  = "login:token:"
+	CAPAccessTokenKey = "cap:access_token"
 )
 
 const (
@@ -19,6 +20,9 @@ func GenerateTokenKey(openId string) string {
 	return fmt.Sprint(LoginTokenPrefix, openId)
 }
 
+func GenerateCAPAccessTokenKey() string {
+	return fmt.Sprint(CAPAccessTokenKey)
+}
 func GenerateReportRefreshKey(source string, id int, modifyTime int64) string {
 	return fmt.Sprintf("%s:%d:%d", source, id, modifyTime)
 }

+ 14 - 5
controllers/base_controller.go

@@ -21,6 +21,7 @@ var (
 )
 
 type WrapData struct {
+	Ret  int `description:"返回状态码"`
 	Msg  string
 	Data interface{}
 }
@@ -37,21 +38,27 @@ type BaseController struct {
 	web.Controller
 }
 
-func (b *BaseController) FailResponse(errInfo error, msg string) {
+func (b *BaseController) FailResponse(retCode int, errInfo error, msg string) {
 	var retData BaseResponse
 	var etaError *exception.EtaError
 	if !errors.As(errInfo, &etaError) {
 		etaError = exception.New(exception.UnknownError)
 	}
 	retData = BaseResponse{
-		Ret:     200,
+		Ret:     retCode,
 		Msg:     msg,
-		ErrMsg:  etaError.ErrorMsg,
+		ErrMsg:  getErrMsg(etaError),
 		ErrCode: etaError.ErrorCode,
 		Data:    nil}
 	b.Data["json"] = retData
 	b.ServeJSON()
 }
+func getErrMsg(etaError *exception.EtaError) string {
+	if etaError.Exception != "" {
+		return etaError.ErrorMsg + "[" + etaError.Exception + "]"
+	}
+	return etaError.ErrorMsg
+}
 
 // JsonResult /*
 func (b *BaseController) JsonResult(status int, errCode int, errMsg string, msg string, success bool, data interface{}) {
@@ -91,14 +98,16 @@ func Wrap(a *BaseController, fn func() (*WrapData, error)) {
 	result, err := fn()
 	if err != nil {
 		logger.Error("%v", err)
-		a.FailResponse(err, result.Msg)
+		a.FailResponse(result.Ret, err, result.Msg)
 		return
 	}
 	a.JsonResult(http.GetHttpStatusByAlias("ok"), http.ErrOK, "", result.Msg, http.Success, result.Data)
 }
 
 func (b *BaseController) InitWrapData(msg string) *WrapData {
-	return &WrapData{Msg: msg}
+	return &WrapData{
+		Ret: http.GetHttpStatusByAlias("ok"),
+		Msg: msg}
 }
 func (b *BaseController) SuccessResult(msg string, data interface{}, wrapData *WrapData) {
 	wrapData.Msg = msg

+ 7 - 0
controllers/user/account_controller.go

@@ -0,0 +1,7 @@
+package user
+
+import "eta/eta_mini_ht_api/controllers"
+
+type AccountController struct {
+	controllers.BaseController
+}

+ 2 - 2
controllers/user/auth_controller.go

@@ -19,7 +19,7 @@ type AuthController struct {
 	controllers.BaseController
 }
 
-const ChinaAreaCode = "86"
+const ChinaAreaCode = "+86"
 
 // LoginReq  获取验证码请求
 type LoginReq struct {
@@ -99,7 +99,7 @@ func (a *AuthController) Login() {
 		//用户手机为空需要绑定手机
 		if userInfo.Mobile == "" {
 			//注册用户或者登录
-			err = auth.BindMobile(userInfo.Id, loginReq.Mobile)
+			err = auth.BindMobile(userInfo.Id, loginReq.Mobile, loginReq.AreaCode)
 			if err != nil {
 				a.FailedResult("登录异常,绑定手机失败", result)
 				return

+ 175 - 0
controllers/user/subscribe_controller.go

@@ -0,0 +1,175 @@
+package user
+
+import (
+	"eta/eta_mini_ht_api/common/exception"
+	"eta/eta_mini_ht_api/controllers"
+	"eta/eta_mini_ht_api/service/facade"
+	"eta/eta_mini_ht_api/service/order"
+	"eta/eta_mini_ht_api/service/user"
+	"golang.org/x/time/rate"
+	"net/http"
+	"sync"
+	"time"
+)
+
+type SubscribeController struct {
+	controllers.BaseController
+}
+
+var (
+	// 初始化限流器
+	orderRateLimiter   = NewUserRateLimiter(rate.Every(5*time.Second), 1)
+	orderNoRateLimiter = NewUserRateLimiter(rate.Every(time.Second), 1)
+)
+
+type UserRateLimiter struct {
+	limiters       sync.Map      // 存储每个用户的限流器
+	defaultLimiter *rate.Limiter // 默认限流器
+}
+
+func NewUserRateLimiter(limit rate.Limit, burst int) *UserRateLimiter {
+	return &UserRateLimiter{
+		defaultLimiter: rate.NewLimiter(limit, burst),
+	}
+}
+
+func (url *UserRateLimiter) Allow(userID int) bool {
+	if limiter, ok := url.limiters.Load(userID); ok {
+		return limiter.(*rate.Limiter).Allow()
+	}
+	// 创建新的限流器并存储
+	newLimiter := rate.NewLimiter(url.defaultLimiter.Limit(), url.defaultLimiter.Burst())
+	url.limiters.Store(userID, newLimiter)
+	return newLimiter.Allow()
+}
+
+func orderRateLimitFilter(userId int) (code int) {
+	if userId <= 0 {
+		code = http.StatusBadRequest
+		return
+	}
+	if !orderRateLimiter.Allow(userId) {
+		code = http.StatusTooManyRequests
+		return
+	}
+	code = http.StatusOK
+	return
+}
+func orderNoRateLimitFilter(userId int) (code int) {
+	if userId <= 0 {
+		code = http.StatusBadRequest
+		return
+	}
+	if !orderRateLimiter.Allow(userId) {
+		code = http.StatusTooManyRequests
+		return
+	}
+	code = http.StatusOK
+	return
+}
+
+const (
+	AccountNotOpen = 430
+	IDExpired      = 431
+	RiskUnTest     = 432
+	RiskNotMatch   = 433
+	RiskExpired    = 434
+	AccountOpening = 435
+)
+
+// GetOrderNo  获取订单号
+// @Summary  获取订单号
+// @Description  获取订单号
+// @Success 200 {object} controllers.BaseResponse
+// @router /subscribe/orderNo [get]
+func (sc *SubscribeController) GetOrderNo() {
+	controllers.Wrap(&sc.BaseController, func() (result *controllers.WrapData, err error) {
+		result = sc.InitWrapData("获取订单号失败")
+		userInfo := sc.Data["user"].(user.User)
+		if code := orderNoRateLimitFilter(userInfo.Id); code != 200 {
+			err = exception.New(exception.SubscribeFailed)
+			sc.FailedResult("操作太频繁了,请稍后再试", result)
+			return
+		}
+
+		orderNo := order.GenerateProductOrderNo()
+		sc.SuccessResult("获取订单号成功", orderNo, result)
+		return
+	})
+}
+
+// SubscribeProduct  订阅产品
+// @Summary 订阅产品
+// @Description 订阅产品
+// @Success 200 {object} controllers.BaseResponse
+// @router /subscribe/createOrder [post]
+func (sc *SubscribeController) SubscribeProduct() {
+	controllers.Wrap(&sc.BaseController, func() (result *controllers.WrapData, err error) {
+		result = sc.InitWrapData("订阅产品失败")
+		subscribeReq := new(SubscribeRequest)
+		sc.GetPostParams(subscribeReq)
+		var userInfo user.User
+		userInfo = sc.Data["user"].(user.User)
+		if subscribeReq.ProductId <= 0 {
+			err = exception.New(exception.IllegalProductId)
+			sc.FailedResult("订阅产品失败,产品编号非法", result)
+			return
+		}
+		if subscribeReq.OrderNo == "" {
+			err = exception.New(exception.IllegalOrderNo)
+			sc.FailedResult("订阅产品失败,订单编号非法", result)
+			return
+		}
+		if code := orderRateLimitFilter(userInfo.Id); code != 200 {
+			err = exception.New(exception.SubscribeFailed)
+			sc.FailedResult("操作太频繁了,请稍后再试", result)
+			return
+		}
+		//是否开户
+		//未开户
+		officialUser, err := user.GetUserByTemplateUserId(userInfo.Id)
+		if err != nil {
+			if err.Error() == exception.GetMsg(exception.OfficialUserNotFound) {
+				result.Ret = AccountNotOpen
+				sc.FailedResult("用户未开通账户", result)
+			} else {
+				sc.FailedResult("订阅产品失败", result)
+			}
+			return
+		}
+		//开户中
+		if officialUser.AccountStatus == "opening" {
+			result.Ret = AccountOpening
+			sc.FailedResult("用户开户中", result)
+			return
+		}
+
+		//证件信息是否过期
+		if !officialUser.IDValid {
+			result.Ret = IDExpired
+			sc.FailedResult("用户证件有效期过期", result)
+			return
+		}
+		//主动发起查询最新风险测评(获取失败用系统中原有的测评信息)
+		code, err := facade.CheckUserRiskLevel(userInfo.Id, subscribeReq.ProductId)
+		if err != nil {
+			result.Ret = code
+			sc.FailedResult("校验用户风险等级失败", result)
+			return
+		}
+		//创单
+		//返回订单信息、商品信息
+		orderInfo, err := order.CreateProductOrder(userInfo, subscribeReq.ProductId, subscribeReq.OrderNo)
+		if err != nil {
+			sc.FailedResult("创建订单失败", result)
+			return
+		}
+		sc.SuccessResult("订阅产品成功", orderInfo, result)
+		return
+	})
+}
+
+type SubscribeRequest struct {
+	ProductId int    `json:"productId"`
+	OrderNo   string `json:"orderNo"`
+}

+ 66 - 0
domian/order/product_order.go

@@ -0,0 +1,66 @@
+package order
+
+import (
+	orderDao "eta/eta_mini_ht_api/models/order"
+	"fmt"
+	"math/rand"
+	"time"
+)
+
+type ProductOrderDTO struct {
+	ID            int
+	OrderID       string
+	UserID        int
+	ProductID     int
+	TotalAmount   string
+	TransactionID int
+	RefundAmount  string
+	PaymentTime   time.Time
+	Status        string
+	RefundStatus  string
+	Remark        string
+}
+
+func GenerateProductOrderNo() string {
+	timestamp := time.Now().UnixNano() / 1000000 // 毫秒级时间戳
+	// 生成随机数
+	rand.New(rand.NewSource(time.Now().UnixNano()))
+	randomPart := rand.Intn(999999)
+	// 格式化订单号
+	orderNumber := fmt.Sprintf("P%d%06d", timestamp, randomPart)
+	return orderNumber
+}
+
+func CreateProductOrder(orderDTO ProductOrderDTO) (orderNo string, err error) {
+	order := convertProductOrder(orderDTO)
+	orderNo, err = orderDao.CreateProductOrder(order)
+	return
+}
+
+func convertProductOrderDTO(order orderDao.ProductOrder) ProductOrderDTO {
+	return ProductOrderDTO{
+		ID:            order.ID,
+		OrderID:       order.OrderID,
+		UserID:        order.UserID,
+		ProductID:     order.ProductID,
+		TotalAmount:   order.TotalAmount,
+		TransactionID: order.TransactionID,
+		RefundAmount:  order.RefundAmount,
+		PaymentTime:   order.PaymentTime,
+		Remark:        order.Remark,
+	}
+}
+
+func convertProductOrder(order ProductOrderDTO) orderDao.ProductOrder {
+	return orderDao.ProductOrder{
+		ID:            order.ID,
+		OrderID:       order.OrderID,
+		UserID:        order.UserID,
+		ProductID:     order.ProductID,
+		TotalAmount:   order.TotalAmount,
+		TransactionID: order.TransactionID,
+		RefundAmount:  order.RefundAmount,
+		PaymentTime:   order.PaymentTime,
+		Remark:        order.Remark,
+	}
+}

+ 34 - 2
domian/user/user_serivce.go

@@ -5,6 +5,7 @@ import (
 	logger "eta/eta_mini_ht_api/common/component/log"
 	userDao "eta/eta_mini_ht_api/models/user"
 	"gorm.io/gorm"
+	"time"
 )
 
 type userStatus string
@@ -18,6 +19,7 @@ const (
 type UserDTO struct {
 	Id               int
 	Username         string
+	AreaCode         string
 	Mobile           string
 	OpenId           string
 	UnionId          string
@@ -28,6 +30,15 @@ type UserDTO struct {
 	GzhOpenId        string
 }
 
+type OfficialUserDTO struct {
+	ID             int
+	IdNo           string
+	RealName       string
+	TemplateUserID int
+	IdKind         int
+	IDValid        bool
+	AccountStatus  string
+}
 type FeedbackDTO struct {
 	Mobile  string `json:"mobile"`
 	UserId  int    `json:"userId"`
@@ -38,6 +49,7 @@ func convertUserDTO(user userDao.TemplateUser) UserDTO {
 	return UserDTO{
 		Id:               user.Id,
 		Username:         user.Username,
+		AreaCode:         user.AreaCode,
 		Mobile:           user.Mobile,
 		OpenId:           user.OpenId,
 		LoginStatus:      user.LoginStatus,
@@ -46,6 +58,20 @@ func convertUserDTO(user userDao.TemplateUser) UserDTO {
 		FollowingGzh:     user.FollowingGzh,
 	}
 }
+
+func convertOfficialUserDTO(user userDao.User) OfficialUserDTO {
+	year, month, day := time.Now().Date()
+	today := time.Date(year, month, day, 0, 0, 0, 0, time.Local)
+	return OfficialUserDTO{
+		ID:             user.ID,
+		RealName:       user.RealName,
+		IdNo:           user.IDNo,
+		TemplateUserID: user.TemplateUserID,
+		IDValid:        !user.IDEndDate.Before(today),
+		IdKind:         user.IDKind,
+		AccountStatus:  user.AccountStatus,
+	}
+}
 func GetUserByMobile(mobile string) (dto UserDTO, err error) {
 	templateUser, err := userDao.GetUserByMobile(mobile)
 	if err != nil {
@@ -128,8 +154,8 @@ func convertToUserDTO(templateUser userDao.TemplateUser, dto *UserDTO) {
 	dto.FollowingGzh = templateUser.FollowingGzh
 }
 
-func BindUserMobile(userId int, mobile string) (err error) {
-	err = userDao.BindMobile(userId, mobile)
+func BindUserMobile(userId int, mobile string, areaCode string) (err error) {
+	err = userDao.BindMobile(userId, mobile, areaCode)
 	if err != nil {
 		logger.Error("绑定手机号失败:%v", err)
 	}
@@ -203,3 +229,9 @@ func GetTemplateUserByGzhOpenId(openId string) (user UserDTO, err error) {
 	user = convertUserDTO(templateUser)
 	return
 }
+
+func GetUserByTemplateUserId(templateUserId int) (officialUser OfficialUserDTO, err error) {
+	user, err := userDao.GetUserByTemplateUserId(templateUserId)
+	officialUser = convertOfficialUserDTO(user)
+	return
+}

+ 1 - 0
go.mod

@@ -43,6 +43,7 @@ require (
 	golang.org/x/net v0.23.0 // indirect
 	golang.org/x/sys v0.21.0 // indirect
 	golang.org/x/text v0.16.0 // indirect
+	golang.org/x/time v0.7.0 // indirect
 	google.golang.org/protobuf v1.34.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )

+ 2 - 0
go.sum

@@ -173,6 +173,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
 golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
+golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
+golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=

+ 28 - 1
main.go

@@ -1,11 +1,14 @@
 package main
 
 import (
+	"eta/eta_mini_ht_api/api"
 	_ "eta/eta_mini_ht_api/common/component"
+	"eta/eta_mini_ht_api/common/component/cache"
 	"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/exception"
+	"eta/eta_mini_ht_api/common/utils/redis"
 	"eta/eta_mini_ht_api/domian/report"
 	"eta/eta_mini_ht_api/models/eta"
 	"eta/eta_mini_ht_api/models/ht"
@@ -16,6 +19,11 @@ import (
 	"time"
 )
 
+var (
+	redisUtils = cache.GetInstance()
+	htApi      = api.GetInstance()
+)
+
 func main() {
 	htConfig := config.GetConfig(contants.HT).(*config.HTBizConfig)
 	if web.BConfig.RunMode == "dev" {
@@ -31,11 +39,30 @@ func main() {
 			//初始化研报库
 			initReport()
 		}
+		//初始化第三方AccessToken
+		initThirdPartyAccessToken()
 	}()
 	logger.Info("初始化成功")
 	web.Run()
 }
-
+func initThirdPartyAccessToken() {
+	accessToken := redis.GenerateCAPAccessTokenKey()
+	if redisUtils.GetString(accessToken) == "" {
+		logger.Info("开始初始化CAP AccessToken")
+		token, err := htApi.GetAccessToken()
+		if err != nil {
+			logger.Error("获取CAP AccessToken失败:%v", err)
+			return
+		}
+		expireTime, err := time.Parse(time.DateTime, token.ExpiresAt)
+		if err != nil {
+			logger.Error("解析过期时间失败:%v", err)
+			return
+		}
+		duration := expireTime.Sub(time.Now()).Seconds()
+		_ = redisUtils.SetString(accessToken, token.AccessToken, int(duration))
+	}
+}
 func initReport() {
 	var wg sync.WaitGroup
 	wg.Add(2)

+ 1 - 0
middleware/auth_middleware.go

@@ -79,6 +79,7 @@ var privateRoutes = []string{
 	"/report/count",
 	"/product/*",
 	"/order/*",
+	"/user/subscribe/*",
 }
 
 func AuthMiddleware() web.FilterFunc {

+ 0 - 1
models/merchant/merchant_product.go

@@ -56,7 +56,6 @@ func (m *MerchantProduct) BeforeCreate(_ *gorm.DB) (err error) {
 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
 }
 

+ 60 - 0
models/order/product_order.go

@@ -0,0 +1,60 @@
+package order
+
+import (
+	"errors"
+	"eta/eta_mini_ht_api/models"
+	"github.com/go-sql-driver/mysql"
+	"gorm.io/gorm"
+	"time"
+)
+
+type OrderStatus string
+type RefundStatus string
+
+const (
+	//订单状态
+	OrderStatusPending    OrderStatus = "pending"
+	OrderStatusProcessing OrderStatus = "processing"
+	OrderStatusPaid       OrderStatus = "paid"
+	OrderStatusClosed     OrderStatus = "closed"
+	OrderStatusRefunded   OrderStatus = "refunded"
+	//退款状态
+	RefundStatusSuccess   RefundStatus = "success"
+	RefundStatusPending   RefundStatus = "pending"
+	RefundStatusFailed    RefundStatus = "failed"
+	RefundtatusProcessing RefundStatus = "processing"
+)
+
+type ProductOrder struct {
+	ID            int          `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
+	OrderID       string       `gorm:"column:order_id;size:255;comment:'订单编号'" json:"order_id"`
+	UserID        int          `gorm:"column:user_id;default:null;comment:'用户id'" json:"user_id"`
+	ProductID     int          `gorm:"column:product_id;default:null;comment:'产品id'" json:"product_id"`
+	TotalAmount   string       `gorm:"column:total_amount;size:255;default:null;comment:'金额'" json:"total_amount"`
+	TransactionID int          `gorm:"column:transaction_id;default:null;comment:'支付订单'" json:"transaction_id"`
+	RefundAmount  string       `gorm:"column:refund_amount;size:255;default:null;comment:'退款金额'" json:"refund_amount"`
+	PaymentTime   time.Time    `gorm:"column:payment_time;default:null;comment:'支付时间'" json:"payment_time"`
+	Status        OrderStatus  `gorm:"column:status;type:enum('pending','processing','paid','closed','refunded');default:'pending';comment:'订单状态'" json:"status"`
+	RefundStatus  RefundStatus `gorm:"column:refund_status;type:enum('pending','processing','failed','success');default:'pending';comment:'退款状态'" json:"refund_status"`
+	Remark        string       `gorm:"column:remark;size:255;default:null;comment:'备注'" json:"remark"`
+	IsDeleted     int          `gorm:"column:is_deleted;size:1;default:0;comment:'是否删除'" json:"is_deleted"`
+	CreatedTime   time.Time    `gorm:"column:created_time;default:null;comment:'创建时间'" json:"created_time"`
+	UpdateTime    time.Time    `gorm:"column:update_time;default:null;comment:'更新时间'" json:"update_time"`
+}
+
+func (pr *ProductOrder) BeforeCreate(db *gorm.DB) (err error) {
+	pr.Status = OrderStatusPending
+	pr.CreatedTime = time.Now()
+	pr.IsDeleted = 0
+	return
+}
+func CreateProductOrder(order ProductOrder) (orderNo string, err error) {
+	db := models.Main()
+	err = db.Create(&order).Error
+	var mysqlErr *mysql.MySQLError
+	if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
+		err = gorm.ErrDuplicatedKey
+	}
+	orderNo = order.OrderID
+	return
+}

+ 7 - 5
models/user/template_user.go

@@ -14,13 +14,15 @@ const (
 type AccountStatus string
 
 const (
-	Open   AccountStatus = "open"
-	Unopen AccountStatus = "unOpen"
+	Open    AccountStatus = "open"
+	Opening AccountStatus = "opening"
+	UnOpen  AccountStatus = "unOpen"
 )
 
 type TemplateUser struct {
 	Id               int           `gorm:"column:id;primaryKey;autoIncrement:'id'"`
 	Username         string        `gorm:"column:username;type:varchar(20);comment:用户名"`
+	AreaCode         string        `gorm:"column:area_code;type varchar(10);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"`
@@ -47,7 +49,7 @@ func (t *TemplateUser) BeforeCreate(tx *gorm.DB) (err error) {
 	t.CreatedTime = time.Now()
 	t.LoginStatus = Logout
 	t.IsDeleted = NotDeleted
-	t.AccountStatus = Unopen
+	t.AccountStatus = UnOpen
 	return
 }
 
@@ -78,8 +80,8 @@ func RegisterTemplateUser(user *TemplateUser) (err error) {
 	return
 }
 
-func BindMobile(userId int, mobile string) (err error) {
-	err = models.Main().Table("template_users").Where("id = ?", userId).Updates(map[string]interface{}{"mobile": mobile, "updated_time": time.Now()}).Error
+func BindMobile(userId int, mobile string, areaCode string) (err error) {
+	err = models.Main().Table("template_users").Where("id = ?", userId).Updates(map[string]interface{}{"area_code": areaCode, "mobile": mobile, "updated_time": time.Now()}).Error
 	return
 }
 

+ 29 - 0
models/user/user.go

@@ -0,0 +1,29 @@
+package user
+
+import (
+	"eta/eta_mini_ht_api/models"
+	"time"
+)
+
+const (
+	defaultColumn = "id,real_name,id_no,id_end_date,account_status"
+)
+
+type User struct {
+	ID             int       `gorm:"column:id;primaryKey"`
+	TemplateUserID int       `gorm:"column:template_user_id"`
+	RealName       string    `gorm:"column:real_name;size:255"`
+	IDNo           string    `gorm:"column:id_no;size:15"`
+	IDKind         int       `gorm:"column:id_kind"`
+	IDBeginDate    time.Time `gorm:"column:id_begin_date"`
+	IDEndDate      time.Time `gorm:"column:id_end_date"`
+	AccountStatus  string    `gorm:"column:account_status;type:enum('unopen','opening','opened')"`
+	CreatedTime    time.Time `gorm:"column:created_time"`
+	UpdatedTime    time.Time `gorm:"column:updated_time"`
+}
+
+func GetUserByTemplateUserId(templateUserId int) (user User, err error) {
+	db := models.Main()
+	err = db.Select(defaultColumn).Where("template_user_id=?", templateUserId).First(&user).Error
+	return
+}

+ 18 - 0
routers/commentsRouter.go

@@ -265,6 +265,24 @@ func init() {
             Filters: nil,
             Params: nil})
 
+    beego.GlobalControllerRouter["eta/eta_mini_ht_api/controllers/user:SubscribeController"] = append(beego.GlobalControllerRouter["eta/eta_mini_ht_api/controllers/user:SubscribeController"],
+        beego.ControllerComments{
+            Method: "SubscribeProduct",
+            Router: `/subscribe/createOrder`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_mini_ht_api/controllers/user:SubscribeController"] = append(beego.GlobalControllerRouter["eta/eta_mini_ht_api/controllers/user:SubscribeController"],
+        beego.ControllerComments{
+            Method: "GetOrderNo",
+            Router: `/subscribe/orderNo`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
     beego.GlobalControllerRouter["eta/eta_mini_ht_api/controllers/user:UserController"] = append(beego.GlobalControllerRouter["eta/eta_mini_ht_api/controllers/user:UserController"],
         beego.ControllerComments{
             Method: "Get",

+ 1 - 0
routers/router.go

@@ -32,6 +32,7 @@ func init() {
 		web.NSNamespace("/user",
 			web.NSInclude(
 				&user.UserController{},
+				&user.SubscribeController{},
 			),
 		),
 		web.NSNamespace("/auth",

+ 2 - 2
service/auth/auth_service.go

@@ -188,8 +188,8 @@ func Login(userId int, openId, mobile, verifyCode string) (token string, err err
 	return
 }
 
-func BindMobile(userId int, mobile string) (err error) {
-	err = userService.BindUserMobile(userId, mobile)
+func BindMobile(userId int, mobile string, areaCode string) (err error) {
+	err = userService.BindUserMobile(userId, mobile, areaCode)
 	if err != nil {
 		err = exception.New(exception.BindMobileFailed)
 		return

+ 121 - 10
service/facade/ht_account_service.go

@@ -1,20 +1,25 @@
 package facade
 
 import (
-	"eta/eta_mini_ht_api/common/component/config"
-	"eta/eta_mini_ht_api/common/contants"
-	"eta/eta_mini_ht_api/common/utils/client"
+	"eta/eta_mini_ht_api/api"
+	logger "eta/eta_mini_ht_api/common/component/log"
+	"eta/eta_mini_ht_api/common/exception"
+	permissionService "eta/eta_mini_ht_api/domian/config"
+	"eta/eta_mini_ht_api/service/product"
+	"eta/eta_mini_ht_api/service/user"
+	"fmt"
+	"strconv"
+	"strings"
 )
 
 var (
-	httpClient client.HttpClient
-	htConfig   = config.GetConfig(contants.HT).(*config.HTBizConfig)
-	urlPrefix  = htConfig.GetAccountInfoUrl()
+	htApi = api.GetInstance()
 )
 
 const (
-	clientSuitInfoUrl = "open/api/getClientSuitInfo"
-	authorizationUrl  = "authless/token"
+	RiskUnTest   = 432
+	RiskNotMatch = 433
+	RiskExpired  = 434
 )
 
 func SyncRiskInfo(mobile string) (info string, err error) {
@@ -22,6 +27,112 @@ func SyncRiskInfo(mobile string) (info string, err error) {
 	return "", nil
 }
 
-func accessToken() {
-	httpClient.Post(urlPrefix+authorizationUrl, nil)
+func CheckUserRiskLevel(templateUserId int, productId int) (code int, err error) {
+	//是否完成风测
+	//风测是否过期
+	//风测等级是否符合
+	if productId <= 0 {
+		logger.Error("产品id异常:%v", productId)
+		code = 200
+		err = exception.New(exception.IllegalProductId)
+		return
+	}
+	if templateUserId <= 0 {
+		logger.Error("用户id异常:%v", templateUserId)
+		code = 200
+		err = exception.New(exception.IllegalTemplateUserId)
+		return
+	}
+	product, err := product.GetProductInfoById(productId)
+	if err != nil {
+		logger.Error("获取产品信息失败:%v", err)
+		code = 200
+		err = exception.New(exception.ProductInfoError)
+		return
+	}
+	userInfo, userErr := user.GetUserProfile(templateUserId)
+	if userErr != nil {
+		logger.Error("获取临时用户信息失败:%v,userId:%v", userErr, templateUserId)
+	}
+	officialUser, err := user.GetUserByTemplateUserId(templateUserId)
+	if err != nil {
+		if userErr != nil {
+			logger.Error("获取正式用户信息失败:%v,templateUserId:%v", userErr, templateUserId)
+			code = 200
+			err = exception.New(exception.OfficialUserFoundError)
+			return
+
+		}
+	}
+	customerInfo, err := htApi.GetCustomerRiskLevelInfo(api.ClientSuitInfoReq{
+		ClientName: officialUser.RealName,
+		IdKind:     officialUser.IdKind,
+		IdNo:       officialUser.IdNo,
+	})
+
+	if err != nil {
+		logger.Error("api获取客户风险信息失败:%v", err)
+		if userInfo.RiskLevelStatus == user.RiskUnTest {
+			logger.Error("客户风险等级未测试,mobile:%v", userInfo.Mobile)
+			code = RiskUnTest
+			err = exception.New(exception.RiskUnTestError)
+			return
+		}
+		if userInfo.RiskLevelStatus == user.RiskExpired {
+			logger.Error("客户风险等级已过期,mobile:%v", userInfo.Mobile)
+			code = RiskExpired
+			err = exception.New(exception.RiskExpiredError)
+			return
+		}
+		mapping, mappingErr := permissionService.GetRiskMappingByCustomerRiskLevel(userInfo.RiskLevel)
+		if mappingErr != nil {
+			logger.Error("查询产品风险等级映射失败:%v", mappingErr)
+			code = 200
+			err = exception.New(exception.QueryRiskMappingError)
+			return
+		}
+		return compareRisk(mapping.ProductRiskLevel, product.RiskLevel)
+	}
+	mapping, mappingErr := permissionService.GetRiskMappingByCustomerRiskLevel(customerInfo.RiskInfo.CorpRiskLevel)
+	if mappingErr != nil {
+		logger.Error("查询产品风险等级映射失败:%v", mappingErr)
+		code = 200
+		err = exception.New(exception.IllegalProductId)
+		return
+	}
+	return compareRisk(mapping.ProductRiskLevel, product.RiskLevel)
+}
+func parseRiskLevel(level string) (int, error) {
+	parts := strings.Split(level, "R")
+	if len(parts) < 2 {
+		return 0, fmt.Errorf("无效的风险等级: %s", level)
+	}
+	numberStr := parts[1]
+	number, err := strconv.Atoi(numberStr)
+	if err != nil {
+		return 0, fmt.Errorf("无法将风险等级转换为数字: %s", err)
+	}
+	return number, nil
+}
+func compareRisk(cusPdRisk string, pdRisk string) (code int, err error) {
+	riskLevelNum, err := parseRiskLevel(cusPdRisk)
+	if err != nil {
+		logger.Error("客户风险等级映射风险等级解析失败:%v,risk:%v", err, cusPdRisk)
+		code = 200
+		err = exception.New(exception.QueryRiskMappingError)
+		return
+	}
+	pRiskNum, riskErr := parseRiskLevel(pdRisk)
+	if riskErr != nil {
+		logger.Error("产品风险等级解析失败:risk:%v", pdRisk)
+		code = 200
+		err = exception.New(exception.QueryRiskMappingError)
+		return
+	}
+	if pRiskNum > riskLevelNum {
+		code = RiskNotMatch
+		err = exception.New(exception.RiskUnMatchError)
+		return
+	}
+	return
 }

+ 87 - 0
service/order/order_service.go

@@ -0,0 +1,87 @@
+package order
+
+import (
+	"errors"
+	logger "eta/eta_mini_ht_api/common/component/log"
+	"eta/eta_mini_ht_api/common/exception"
+	productService "eta/eta_mini_ht_api/domian/merchant"
+	orderService "eta/eta_mini_ht_api/domian/order"
+	userService "eta/eta_mini_ht_api/domian/user"
+	"eta/eta_mini_ht_api/service/user"
+	"gorm.io/gorm"
+	"time"
+)
+
+type ProductOrderInfo struct {
+	Buyer       BuyerInfo   `json:"buyer"`
+	ProductInfo ProductInfo `json:"productInfo"`
+	OrderNo     string      `json:"orderNo"`
+}
+type ProductInfo struct {
+	Name      string
+	Type      string
+	Price     string
+	BeginDate string
+	EndDate   string
+}
+type BuyerInfo struct {
+	Name     string
+	Mobile   string
+	AreaCode string
+}
+
+func GenerateProductOrderNo() string {
+	return orderService.GenerateProductOrderNo()
+}
+func CreateProductOrder(templateUser user.User, productId int, orderNo string) (orderInfo ProductOrderInfo, err error) {
+	if productId <= 0 {
+		err = exception.New(exception.IllegalProductId)
+	}
+	productInfo, err := productService.GetMerchantProductById(productId)
+	if err != nil {
+		err = exception.NewWithException(exception.ProductInfoError, err.Error())
+		return
+	}
+	officialUser, err := userService.GetUserByTemplateUserId(templateUser.Id)
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			err = exception.New(exception.OfficialUserNotFound)
+		} else {
+			err = exception.NewWithException(exception.OfficialUserFoundError, err.Error())
+		}
+		return
+	}
+	orderNo, err = orderService.CreateProductOrder(orderService.ProductOrderDTO{
+		OrderID:     orderNo,
+		UserID:      templateUser.Id,
+		ProductID:   productId,
+		TotalAmount: productInfo.Price.String(),
+	})
+	if err != nil {
+		logger.Error("创建订单失败:%v", err)
+		if errors.Is(err, gorm.ErrDuplicatedKey) {
+			err = exception.NewWithException(exception.SubscribeFailed, "请勿重复下单")
+			return
+		}
+		err = exception.NewWithException(exception.SubscribeFailed, err.Error())
+		return
+	}
+	buyer := BuyerInfo{
+		Name:     officialUser.RealName,
+		Mobile:   templateUser.Mobile,
+		AreaCode: templateUser.AreaCode,
+	}
+	product := ProductInfo{
+		Name:      productInfo.Title,
+		Type:      productInfo.Type,
+		Price:     productInfo.Price.String(),
+		BeginDate: time.Now().Format(time.DateOnly),
+		EndDate:   time.Now().Add(time.Duration(productInfo.ValidDays*24) * time.Hour).Format(time.DateOnly),
+	}
+	orderInfo = ProductOrderInfo{
+		Buyer:       buyer,
+		ProductInfo: product,
+		OrderNo:     orderNo,
+	}
+	return
+}

+ 2 - 0
service/product/product_service.go

@@ -12,6 +12,7 @@ type ProductDTO struct {
 	Description string
 	Price       decimal.Decimal
 	CoverSrc    string
+	RiskLevel   string
 	Type        string
 	BeginDate   string
 	EndDate     string
@@ -33,6 +34,7 @@ func convertToProductDTO(product merchantService.MerchantProductDTO) (productDTO
 		Title:       product.Title,
 		Description: product.Description,
 		Price:       product.Price,
+		RiskLevel:   product.RiskLevel,
 		CoverSrc:    product.CoverSrc,
 		Type:        product.Type,
 		BeginDate:   beginDate.Format(time.DateOnly),

+ 16 - 0
service/user/user_service.go

@@ -21,6 +21,7 @@ const (
 type User struct {
 	Id       int    `json:"id"`
 	Username string `json:"username"`
+	AreaCode string `json:"areaCode"`
 	Mobile   string `json:"mobile"`
 	OpenId   string `json:"openId,omitempty"`
 }
@@ -299,6 +300,21 @@ func convertToUser(userDTO userService.UserDTO) User {
 		Id:       userDTO.Id,
 		Username: userDTO.Username,
 		OpenId:   userDTO.OpenId,
+		AreaCode: userDTO.AreaCode,
 		Mobile:   userDTO.Mobile,
 	}
 }
+
+func GetUserByTemplateUserId(templateUserId int) (officialUser userService.OfficialUserDTO, err error) {
+	officialUser, err = userService.GetUserByTemplateUserId(templateUserId)
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			err = exception.New(exception.OfficialUserNotFound)
+			logger.Info("用户未开户:%v", templateUserId)
+		} else {
+			err = exception.NewWithException(exception.OfficialUserFoundError, err.Error())
+			logger.Error("获取正式用户信息失败:%v", err)
+		}
+	}
+	return
+}