add query url share

This commit is contained in:
liangzai 2025-06-02 18:21:08 +08:00
parent 1bff356eb8
commit bebabce346
13 changed files with 380 additions and 86 deletions

View File

@ -116,8 +116,23 @@ service main {
@doc "确认查询状态" @doc "确认查询状态"
@handler confirmQueryState @handler confirmQueryState
post /query/confirm_state (ConfirmQueryStateReq) returns (ConfirmQueryStateResp) post /query/confirm_state (ConfirmQueryStateReq) returns (ConfirmQueryStateResp)
@doc "生成分享链接"
@handler QueryGenerateShareLink
post /query/generate_share_link (QueryGenerateShareLinkReq) returns (QueryGenerateShareLinkResp)
} }
type (
QueryGenerateShareLinkReq {
OrderId *int64 `json:"order_id,optional"`
OrderNo *string `json:"order_no,optional"`
}
QueryGenerateShareLinkResp {
ShareLink string `json:"share_link"`
}
)
// 获取查询临时订单 // 获取查询临时订单
type ( type (
QueryProvisionalOrderReq { QueryProvisionalOrderReq {
@ -208,15 +223,23 @@ service main {
post /query/single/test (QuerySingleTestReq) returns (QuerySingleTestResp) post /query/single/test (QuerySingleTestReq) returns (QuerySingleTestResp)
@doc "查询详情" @doc "查询详情"
@handler queryDetail @handler queryShareDetail
get /query/:id (QueryDetailReq) returns (QueryDetailResp) get /query/share/:id (QueryShareDetailReq) returns (QueryShareDetailResp)
@doc "查询示例" @doc "查询示例"
@handler queryExample @handler queryExample
get /query/example (QueryExampleReq) returns (QueryExampleResp) get /query/example (QueryExampleReq) returns (QueryExampleResp)
} }
type (
QueryShareDetailReq {
Id string `path:"id"`
}
QueryShareDetailResp {
Status string `json:"status"`
Query
}
)
type QuerySingleTestReq { type QuerySingleTestReq {
Params map[string]interface{} `json:"params"` Params map[string]interface{} `json:"params"`
Api string `json:"api"` Api string `json:"api"`

View File

@ -70,3 +70,5 @@ CloudAuth:
Endpoint: "cloudauth.aliyuncs.com" Endpoint: "cloudauth.aliyuncs.com"
SceneId: 1000013341 SceneId: 1000013341
ReturnUrl: "https://www.quannengcha.com/authorization/result" ReturnUrl: "https://www.quannengcha.com/authorization/result"
Query:
ShareLinkExpire: 604800 # 7天 = 7 * 24 * 60 * 60 = 604800秒

View File

@ -71,3 +71,5 @@ CloudAuth:
Endpoint: "cloudauth.aliyuncs.com" Endpoint: "cloudauth.aliyuncs.com"
SceneId: 1000013341 SceneId: 1000013341
ReturnUrl: "https://www.quannengcha.com/authorization/result" ReturnUrl: "https://www.quannengcha.com/authorization/result"
Query:
ShareLinkExpire: 604800 # 7天 = 7 * 24 * 60 * 60 = 604800秒

View File

@ -21,6 +21,7 @@ type Config struct {
SystemConfig SystemConfig SystemConfig SystemConfig
WechatH5 WechatH5Config WechatH5 WechatH5Config
CloudAuth CloudAuthConfig CloudAuth CloudAuthConfig
Query QueryConfig
} }
// JwtAuth 用于 JWT 鉴权配置 // JwtAuth 用于 JWT 鉴权配置
@ -100,3 +101,6 @@ type CloudAuthConfig struct {
SceneId int64 SceneId int64
ReturnUrl string ReturnUrl string
} }
type QueryConfig struct {
ShareLinkExpire int64
}

View File

@ -0,0 +1,29 @@
package query
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"qnc-server/app/user/cmd/api/internal/logic/query"
"qnc-server/app/user/cmd/api/internal/svc"
"qnc-server/app/user/cmd/api/internal/types"
"qnc-server/common/result"
"qnc-server/pkg/lzkit/validator"
)
func QueryGenerateShareLinkHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.QueryGenerateShareLinkReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := query.NewQueryGenerateShareLinkLogic(r.Context(), svcCtx)
resp, err := l.QueryGenerateShareLink(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@ -3,18 +3,17 @@ package query
import ( import (
"net/http" "net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"qnc-server/app/user/cmd/api/internal/logic/query" "qnc-server/app/user/cmd/api/internal/logic/query"
"qnc-server/app/user/cmd/api/internal/svc" "qnc-server/app/user/cmd/api/internal/svc"
"qnc-server/app/user/cmd/api/internal/types" "qnc-server/app/user/cmd/api/internal/types"
"qnc-server/common/result" "qnc-server/common/result"
"qnc-server/pkg/lzkit/validator" "qnc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
) )
func QueryDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { func QueryShareDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
var req types.QueryDetailReq var req types.QueryShareDetailReq
if err := httpx.Parse(r, &req); err != nil { if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err) result.ParamErrorResult(r, w, err)
return return
@ -23,8 +22,8 @@ func QueryDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
result.ParamValidateErrorResult(r, w, err) result.ParamValidateErrorResult(r, w, err)
return return
} }
l := query.NewQueryDetailLogic(r.Context(), svcCtx) l := query.NewQueryShareDetailLogic(r.Context(), svcCtx)
resp, err := l.QueryDetail(&req) resp, err := l.QueryShareDetail(&req)
result.HttpResult(r, w, resp, err) result.HttpResult(r, w, resp, err)
} }
} }

View File

@ -309,6 +309,12 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/query/confirm_state", Path: "/query/confirm_state",
Handler: query.ConfirmQueryStateHandler(serverCtx), Handler: query.ConfirmQueryStateHandler(serverCtx),
}, },
{
// 生成分享链接
Method: http.MethodPost,
Path: "/query/generate_share_link",
Handler: query.QueryGenerateShareLinkHandler(serverCtx),
},
{ {
// 查询列表 // 查询列表
Method: http.MethodGet, Method: http.MethodGet,
@ -352,18 +358,18 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
server.AddRoutes( server.AddRoutes(
[]rest.Route{ []rest.Route{
{
// 查询详情
Method: http.MethodGet,
Path: "/query/:id",
Handler: query.QueryDetailHandler(serverCtx),
},
{ {
// 查询示例 // 查询示例
Method: http.MethodGet, Method: http.MethodGet,
Path: "/query/example", Path: "/query/example",
Handler: query.QueryExampleHandler(serverCtx), Handler: query.QueryExampleHandler(serverCtx),
}, },
{
// 查询详情
Method: http.MethodGet,
Path: "/query/share/:id",
Handler: query.QueryShareDetailHandler(serverCtx),
},
{ {
Method: http.MethodPost, Method: http.MethodPost,
Path: "/query/single/test", Path: "/query/single/test",

View File

@ -1,70 +0,0 @@
package query
import (
"context"
"encoding/hex"
"encoding/json"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto"
"qnc-server/pkg/lzkit/lzUtils"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
"qnc-server/app/user/cmd/api/internal/svc"
"qnc-server/app/user/cmd/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type QueryDetailLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewQueryDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryDetailLogic {
return &QueryDetailLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *QueryDetailLogic) QueryDetail(req *types.QueryDetailReq) (resp *types.QueryDetailResp, err error) {
queryModel, err := l.svcCtx.QueryModel.FindOne(l.ctx, req.Id)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "报告查询, 查找报告错误: %v", err)
}
var query types.Query
query.CreateTime = queryModel.CreateTime.Format("2006-01-02 15:04:05")
query.UpdateTime = queryModel.UpdateTime.Format("2006-01-02 15:04:05")
secretKey := l.svcCtx.Config.Encrypt.SecretKey
key, decodeErr := hex.DecodeString(secretKey)
if decodeErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 获取AES解密解药失败, %v", err)
}
if lzUtils.NullStringToString(queryModel.QueryData) != "" {
queryData, decryptErr := crypto.AesDecrypt(lzUtils.NullStringToString(queryModel.QueryData), key)
if decryptErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告结果解密失败, %+v", decryptErr)
}
unmarshalErr := json.Unmarshal(queryData, &query.QueryData)
if unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告结构体处理失败, %+v", unmarshalErr)
}
}
err = copier.Copy(&query, queryModel)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告结构体复制失败, %v", err)
}
product, err := l.svcCtx.ProductModel.FindOne(l.ctx, queryModel.ProductId)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 获取商品信息失败, %v", err)
}
query.ProductName = product.ProductName
return &types.QueryDetailResp{
Query: query,
}, nil
}

View File

@ -0,0 +1,111 @@
package query
import (
"context"
"encoding/hex"
"encoding/json"
"time"
"qnc-server/app/user/cmd/api/internal/svc"
"qnc-server/app/user/cmd/api/internal/types"
"qnc-server/app/user/model"
"qnc-server/common/ctxdata"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
type QueryGenerateShareLinkLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewQueryGenerateShareLinkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryGenerateShareLinkLogic {
return &QueryGenerateShareLinkLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *QueryGenerateShareLinkLogic) QueryGenerateShareLink(req *types.QueryGenerateShareLinkReq) (resp *types.QueryGenerateShareLinkResp, err error) {
userId, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 获取用户ID失败: %v", err)
}
// 检查参数
if (req.OrderId == nil || *req.OrderId == 0) && (req.OrderNo == nil || *req.OrderNo == "") {
return nil, errors.Wrapf(xerr.NewErrMsg("订单ID和订单号不能同时为空"), "")
}
var order *model.Order
// 优先使用OrderId查询
if req.OrderId != nil && *req.OrderId != 0 {
order, err = l.svcCtx.OrderModel.FindOne(l.ctx, *req.OrderId)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("订单不存在"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 获取订单失败: %v", err)
}
} else if req.OrderNo != nil && *req.OrderNo != "" {
// 使用OrderNo查询
order, err = l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, *req.OrderNo)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("订单不存在"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 获取订单失败: %v", err)
}
} else {
return nil, errors.Wrapf(xerr.NewErrMsg("订单ID和订单号不能同时为空"), "")
}
if order.Status != model.OrderStatusPaid {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 订单未支付")
}
query, err := l.svcCtx.QueryModel.FindOneByOrderId(l.ctx, order.Id)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 获取查询失败: %v", err)
}
if query.QueryState != model.QueryStateSuccess {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 查询未成功")
}
user, err := l.svcCtx.UserModel.FindOne(l.ctx, userId)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 获取用户失败: %v", err)
}
if user.Inside != 1 {
if order.UserId != userId {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 无权操作此订单")
}
}
expireAt := time.Now().Add(time.Duration(l.svcCtx.Config.Query.ShareLinkExpire) * time.Second)
payload := types.QueryShareLinkPayload{
OrderId: order.Id, // 使用查询到的订单ID
ExpireAt: expireAt.Unix(),
}
secretKey := l.svcCtx.Config.Encrypt.SecretKey
key, err := hex.DecodeString(secretKey)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 解密失败: %v", err)
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 序列化失败: %v", err)
}
encryptedPayload, err := crypto.AesEncryptURL(payloadBytes, key)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 加密失败: %v", err)
}
return &types.QueryGenerateShareLinkResp{
ShareLink: encryptedPayload,
}, nil
}

View File

@ -0,0 +1,164 @@
package query
import (
"context"
"encoding/hex"
"fmt"
"time"
"qnc-server/app/user/cmd/api/internal/svc"
"qnc-server/app/user/cmd/api/internal/types"
"qnc-server/app/user/model"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto"
"github.com/bytedance/sonic"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
type QueryShareDetailLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewQueryShareDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryShareDetailLogic {
return &QueryShareDetailLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *QueryShareDetailLogic) QueryShareDetail(req *types.QueryShareDetailReq) (resp *types.QueryShareDetailResp, err error) {
secretKey := l.svcCtx.Config.Encrypt.SecretKey
key, decodeErr := hex.DecodeString(secretKey)
if decodeErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 获取AES解密解药失败, %v", err)
}
decryptedID, decryptErr := crypto.AesDecryptURL(req.Id, key)
if decryptErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 解密数据失败: %v", decryptErr)
}
var payload types.QueryShareLinkPayload
err = sonic.Unmarshal(decryptedID, &payload)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 解密数据失败: %v", err)
}
// 检查分享链接是否过期
now := time.Now().Unix()
if now > payload.ExpireAt {
return &types.QueryShareDetailResp{
Status: "expired",
}, nil
}
// 获取订单信息
order, err := l.svcCtx.OrderModel.FindOne(l.ctx, payload.OrderId)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.LOGIC_QUERY_NOT_FOUND), "报告查询, 订单不存在: %v", err)
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "报告查询, 查找报告错误: %v", err)
}
// 检查订单状态
if order.Status != "paid" {
return nil, errors.Wrapf(xerr.NewErrMsg("订单未支付,无法查看报告"), "")
}
// 获取报告信息
queryModel, err := l.svcCtx.QueryModel.FindOneByOrderId(l.ctx, order.Id)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "报告查询, 查找报告错误: %v", err)
}
var query types.Query
query.CreateTime = queryModel.CreateTime.Format("2006-01-02 15:04:05")
query.UpdateTime = queryModel.UpdateTime.Format("2006-01-02 15:04:05")
processParamsErr := ProcessQueryParams(queryModel.QueryParams, &query.QueryParams, key)
if processParamsErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告参数处理失败: %v", processParamsErr)
}
processErr := ProcessQueryData(queryModel.QueryData, &query.QueryData, key)
if processErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告结果处理失败: %v", processErr)
}
updateFeatureAndProductFeatureErr := l.UpdateFeatureAndProductFeature(queryModel.ProductId, &query.QueryData)
if updateFeatureAndProductFeatureErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告结果处理失败: %v", updateFeatureAndProductFeatureErr)
}
// 复制报告数据
err = copier.Copy(&query, queryModel)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告结构体复制失败, %v", err)
}
product, err := l.svcCtx.ProductModel.FindOne(l.ctx, queryModel.ProductId)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 获取商品信息失败, %v", err)
}
query.ProductName = product.ProductName
return &types.QueryShareDetailResp{
Status: "success",
Query: query,
}, nil
}
func (l *QueryShareDetailLogic) UpdateFeatureAndProductFeature(productID int64, target *[]types.QueryItem) error {
// 遍历 target 数组,使用倒序遍历,以便删除元素时不影响索引
for i := len(*target) - 1; i >= 0; i-- {
queryItem := &(*target)[i]
// 确保 Data 为 map 类型
data, ok := queryItem.Data.(map[string]interface{})
if !ok {
return fmt.Errorf("queryItem.Data 必须是 map[string]interface{} 类型")
}
// 从 Data 中获取 apiID
apiID, ok := data["apiID"].(string)
if !ok {
return fmt.Errorf("queryItem.Data 中的 apiID 必须是字符串类型")
}
// 查询 Feature
feature, err := l.svcCtx.FeatureModel.FindOneByApiId(l.ctx, apiID)
if err != nil {
// 如果 Feature 查不到,也要删除当前 QueryItem
*target = append((*target)[:i], (*target)[i+1:]...)
continue
}
// 查询 ProductFeatureModel
builder := l.svcCtx.ProductFeatureModel.SelectBuilder().Where("product_id = ?", productID)
productFeatures, err := l.svcCtx.ProductFeatureModel.FindAll(l.ctx, builder, "")
if err != nil {
return fmt.Errorf("查询 ProductFeatureModel 错误: %v", err)
}
// 遍历 productFeatures找到与 feature.ID 关联且 enable == 1 的项
var featureData map[string]interface{}
// foundFeature := false
sort := 0
for _, pf := range productFeatures {
if pf.FeatureId == feature.Id { // 确保和 Feature 关联
sort = int(pf.Sort)
break // 找到第一个符合条件的就退出循环
}
}
featureData = map[string]interface{}{
"featureName": feature.Name,
"sort": sort,
}
// 更新 queryItem 的 Feature 字段(不是数组)
queryItem.Feature = featureData
}
return nil
}

View File

@ -0,0 +1,6 @@
package types
type QueryShareLinkPayload struct {
OrderId int64 `json:"order_id"`
ExpireAt int64 `json:"expire_at"`
}

View File

@ -424,6 +424,15 @@ type QueryExampleResp struct {
Query Query
} }
type QueryGenerateShareLinkReq struct {
OrderId *int64 `json:"order_id,optional"`
OrderNo *string `json:"order_no,optional"`
}
type QueryGenerateShareLinkResp struct {
ShareLink string `json:"share_link"`
}
type QueryItem struct { type QueryItem struct {
Feature interface{} `json:"feature"` Feature interface{} `json:"feature"`
Data interface{} `json:"data"` // 这里可以是 map 或 具体的 struct Data interface{} `json:"data"` // 这里可以是 map 或 具体的 struct
@ -492,6 +501,15 @@ type QueryServiceResp struct {
RefreshAfter int64 `json:"refreshAfter"` RefreshAfter int64 `json:"refreshAfter"`
} }
type QueryShareDetailReq struct {
Id string `path:"id"`
}
type QueryShareDetailResp struct {
Status string `json:"status"`
Query
}
type QuerySingleTestReq struct { type QuerySingleTestReq struct {
Params map[string]interface{} `json:"params"` Params map[string]interface{} `json:"params"`
Api string `json:"api"` Api string `json:"api"`

View File

@ -8,7 +8,7 @@ import (
func TestGenerateAndParseJwtToken(t *testing.T) { func TestGenerateAndParseJwtToken(t *testing.T) {
// 测试参数 // 测试参数
userId := int64(2012) userId := int64(2043)
secret := "WUvoIwL-FK0qnlxhvxR9tV6SjfOpeJMpKmY2QvT99lA" secret := "WUvoIwL-FK0qnlxhvxR9tV6SjfOpeJMpKmY2QvT99lA"
expireTime := int64(2592000) // 1小时过期 expireTime := int64(2592000) // 1小时过期