Compare commits
8 Commits
11c1391f06
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bbab0e3b5f | |||
| be6d3e5f2c | |||
| 7e2dda986f | |||
| a88154e8e2 | |||
| 9053225fa7 | |||
| 0bfcf25ea8 | |||
| c960fd099a | |||
| e42d2d18aa |
@@ -45,8 +45,10 @@ service main {
|
||||
type (
|
||||
// 创建功能请求
|
||||
AdminCreateFeatureReq {
|
||||
ApiId string `json:"api_id"` // API标识
|
||||
Name string `json:"name"` // 描述
|
||||
ApiId string `json:"api_id"` // API标识
|
||||
Name string `json:"name"` // 描述
|
||||
WhitelistPrice *float64 `json:"whitelist_price,optional"` // 白名单屏蔽价格(单位:元)
|
||||
CostPrice *float64 `json:"cost_price,optional"` // 天远API调用成本价(单位:元)
|
||||
}
|
||||
// 创建功能响应
|
||||
AdminCreateFeatureResp {
|
||||
@@ -54,9 +56,11 @@ type (
|
||||
}
|
||||
// 更新功能请求
|
||||
AdminUpdateFeatureReq {
|
||||
Id string `path:"id"` // 功能ID
|
||||
ApiId *string `json:"api_id,optional"` // API标识
|
||||
Name *string `json:"name,optional"` // 描述
|
||||
Id string `path:"id"` // 功能ID
|
||||
ApiId *string `json:"api_id,optional"` // API标识
|
||||
Name *string `json:"name,optional"` // 描述
|
||||
WhitelistPrice *float64 `json:"whitelist_price,optional"` // 白名单屏蔽价格(单位:元)
|
||||
CostPrice *float64 `json:"cost_price,optional"` // 天远API调用成本价(单位:元)
|
||||
}
|
||||
// 更新功能响应
|
||||
AdminUpdateFeatureResp {
|
||||
@@ -79,11 +83,13 @@ type (
|
||||
}
|
||||
// 功能列表项
|
||||
FeatureListItem {
|
||||
Id string `json:"id"` // 功能ID
|
||||
ApiId string `json:"api_id"` // API标识
|
||||
Name string `json:"name"` // 描述
|
||||
CreateTime string `json:"create_time"` // 创建时间
|
||||
UpdateTime string `json:"update_time"` // 更新时间
|
||||
Id string `json:"id"` // 功能ID
|
||||
ApiId string `json:"api_id"` // API标识
|
||||
Name string `json:"name"` // 描述
|
||||
WhitelistPrice float64 `json:"whitelist_price"` // 白名单屏蔽价格(单位:元)
|
||||
CostPrice float64 `json:"cost_price"` // 天远API调用成本价(单位:元)
|
||||
CreateTime string `json:"create_time"` // 创建时间
|
||||
UpdateTime string `json:"update_time"` // 更新时间
|
||||
}
|
||||
// 获取功能列表响应
|
||||
AdminGetFeatureListResp {
|
||||
@@ -96,11 +102,13 @@ type (
|
||||
}
|
||||
// 获取功能详情响应
|
||||
AdminGetFeatureDetailResp {
|
||||
Id string `json:"id"` // 功能ID
|
||||
ApiId string `json:"api_id"` // API标识
|
||||
Name string `json:"name"` // 描述
|
||||
CreateTime string `json:"create_time"` // 创建时间
|
||||
UpdateTime string `json:"update_time"` // 更新时间
|
||||
Id string `json:"id"` // 功能ID
|
||||
ApiId string `json:"api_id"` // API标识
|
||||
Name string `json:"name"` // 描述
|
||||
WhitelistPrice float64 `json:"whitelist_price"` // 白名单屏蔽价格(单位:元)
|
||||
CostPrice float64 `json:"cost_price"` // 天远API调用成本价(单位:元)
|
||||
CreateTime string `json:"create_time"` // 创建时间
|
||||
UpdateTime string `json:"update_time"` // 更新时间
|
||||
}
|
||||
// 配置功能示例数据请求
|
||||
AdminConfigFeatureExampleReq {
|
||||
|
||||
@@ -123,8 +123,9 @@ type (
|
||||
}
|
||||
// 生成邀请海报(背景图+二维码,返回 base64)
|
||||
GetInvitePosterReq {
|
||||
InviteLink string `form:"invite_link"` // 邀请链接(短链)
|
||||
Format string `form:"format,optional"` // 返回格式:base64(默认)
|
||||
InviteLink string `form:"invite_link"` // 邀请链接(短链)
|
||||
Format string `form:"format,optional"` // 返回格式:base64(默认)
|
||||
TemplateIndex int `form:"template_index,optional"` // 模板序号 1-4,默认 1
|
||||
}
|
||||
GetInvitePosterResp {
|
||||
PosterBase64 string `json:"poster_base64"` // 海报图片 base64(不含 data:image/png;base64, 前缀)
|
||||
@@ -619,11 +620,13 @@ type (
|
||||
List []PromotionQueryItem `json:"list"` // 列表
|
||||
}
|
||||
PromotionQueryItem {
|
||||
Id string `json:"id"` // 查询ID
|
||||
OrderId string `json:"order_id"` // 订单ID
|
||||
ProductName string `json:"product_name"` // 产品名称
|
||||
CreateTime string `json:"create_time"` // 创建时间
|
||||
QueryState string `json:"query_state"` // 查询状态
|
||||
Id string `json:"id"` // 查询ID
|
||||
OrderId string `json:"order_id"` // 订单ID
|
||||
ProductName string `json:"product_name"` // 产品名称
|
||||
CreateTime string `json:"create_time"` // 创建时间
|
||||
QueryState string `json:"query_state"` // 查询状态
|
||||
Params map[string]interface{} `json:"params"` // 查询参数(已脱敏)
|
||||
Price float64 `json:"price"` // 查询价格
|
||||
}
|
||||
// ============================================
|
||||
// 用户模块白名单相关类型
|
||||
|
||||
@@ -34,6 +34,14 @@ func (l *AdminCreateFeatureLogic) AdminCreateFeature(req *types.AdminCreateFeatu
|
||||
ApiId: req.ApiId,
|
||||
Name: req.Name,
|
||||
}
|
||||
// 设置白名单屏蔽价格
|
||||
if req.WhitelistPrice != nil {
|
||||
data.WhitelistPrice = *req.WhitelistPrice
|
||||
}
|
||||
// 设置成本价
|
||||
if req.CostPrice != nil {
|
||||
data.CostPrice = *req.CostPrice
|
||||
}
|
||||
|
||||
// 2. 数据库操作
|
||||
result, err := l.svcCtx.FeatureModel.Insert(l.ctx, nil, data)
|
||||
|
||||
@@ -30,16 +30,18 @@ func (l *AdminGetFeatureDetailLogic) AdminGetFeatureDetail(req *types.AdminGetFe
|
||||
record, err := l.svcCtx.FeatureModel.FindOne(l.ctx, req.Id)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
|
||||
"查找功能失败, err: %v, id: %d", err, req.Id)
|
||||
"查找功能失败, err: %v, id: %s", err, req.Id)
|
||||
}
|
||||
|
||||
// 2. 构建响应
|
||||
resp = &types.AdminGetFeatureDetailResp{
|
||||
Id: record.Id,
|
||||
ApiId: record.ApiId,
|
||||
Name: record.Name,
|
||||
CreateTime: record.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
UpdateTime: record.UpdateTime.Format("2006-01-02 15:04:05"),
|
||||
Id: record.Id,
|
||||
ApiId: record.ApiId,
|
||||
Name: record.Name,
|
||||
WhitelistPrice: record.WhitelistPrice,
|
||||
CostPrice: record.CostPrice,
|
||||
CreateTime: record.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
UpdateTime: record.UpdateTime.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
|
||||
@@ -49,11 +49,13 @@ func (l *AdminGetFeatureListLogic) AdminGetFeatureList(req *types.AdminGetFeatur
|
||||
items := make([]types.FeatureListItem, 0, len(list))
|
||||
for _, item := range list {
|
||||
listItem := types.FeatureListItem{
|
||||
Id: item.Id,
|
||||
ApiId: item.ApiId,
|
||||
Name: item.Name,
|
||||
CreateTime: item.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
UpdateTime: item.UpdateTime.Format("2006-01-02 15:04:05"),
|
||||
Id: item.Id,
|
||||
ApiId: item.ApiId,
|
||||
Name: item.Name,
|
||||
WhitelistPrice: item.WhitelistPrice,
|
||||
CostPrice: item.CostPrice,
|
||||
CreateTime: item.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
UpdateTime: item.UpdateTime.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
items = append(items, listItem)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func (l *AdminUpdateFeatureLogic) AdminUpdateFeature(req *types.AdminUpdateFeatu
|
||||
record, err := l.svcCtx.FeatureModel.FindOne(l.ctx, req.Id)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
|
||||
"查找功能失败, err: %v, id: %d", err, req.Id)
|
||||
"查找功能失败, err: %v, id: %s", err, req.Id)
|
||||
}
|
||||
|
||||
// 3. 直接更新record的字段(只更新非空字段)
|
||||
@@ -46,12 +46,18 @@ func (l *AdminUpdateFeatureLogic) AdminUpdateFeature(req *types.AdminUpdateFeatu
|
||||
if req.Name != nil && *req.Name != "" {
|
||||
record.Name = *req.Name
|
||||
}
|
||||
if req.WhitelistPrice != nil {
|
||||
record.WhitelistPrice = *req.WhitelistPrice
|
||||
}
|
||||
if req.CostPrice != nil {
|
||||
record.CostPrice = *req.CostPrice
|
||||
}
|
||||
|
||||
// 4. 执行更新操作
|
||||
err = l.svcCtx.FeatureModel.UpdateWithVersion(l.ctx, nil, record)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
|
||||
"更新功能失败, err: %v, id: %d", err, req.Id)
|
||||
"更新功能失败, err: %v, id: %s", err, req.Id)
|
||||
}
|
||||
|
||||
// 5. 返回成功结果
|
||||
|
||||
@@ -2,10 +2,11 @@ package admin_product
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"bdqr-server/app/main/api/internal/svc"
|
||||
"bdqr-server/app/main/api/internal/types"
|
||||
"bdqr-server/common/xerr"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
@@ -25,43 +26,121 @@ func NewAdminGetProductListLogic(ctx context.Context, svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func (l *AdminGetProductListLogic) AdminGetProductList(req *types.AdminGetProductListReq) (resp *types.AdminGetProductListResp, err error) {
|
||||
// 1. 构建查询条件
|
||||
builder := l.svcCtx.ProductModel.SelectBuilder()
|
||||
// 1. 构建产品查询条件
|
||||
productBuilder := l.svcCtx.ProductModel.SelectBuilder()
|
||||
|
||||
// 2. 添加查询条件
|
||||
if req.ProductName != nil && *req.ProductName != "" {
|
||||
builder = builder.Where("product_name LIKE ?", "%"+*req.ProductName+"%")
|
||||
productBuilder = productBuilder.Where("product_name LIKE ?", "%"+*req.ProductName+"%")
|
||||
}
|
||||
if req.ProductEn != nil && *req.ProductEn != "" {
|
||||
builder = builder.Where("product_en LIKE ?", "%"+*req.ProductEn+"%")
|
||||
productBuilder = productBuilder.Where("product_en LIKE ?", "%"+*req.ProductEn+"%")
|
||||
}
|
||||
|
||||
// 3. 执行分页查询
|
||||
list, total, err := l.svcCtx.ProductModel.FindPageListByPageWithTotal(
|
||||
l.ctx, builder, req.Page, req.PageSize, "id DESC")
|
||||
// 3. 执行产品分页查询
|
||||
productList, total, err := l.svcCtx.ProductModel.FindPageListByPageWithTotal(
|
||||
l.ctx, productBuilder, req.Page, req.PageSize, "id DESC")
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
|
||||
"查询产品列表失败, err: %v, req: %+v", err, req)
|
||||
}
|
||||
|
||||
// 4. 构建响应列表
|
||||
items := make([]types.ProductListItem, 0, len(list))
|
||||
for _, item := range list {
|
||||
if len(productList) == 0 {
|
||||
return &types.AdminGetProductListResp{
|
||||
Total: total,
|
||||
Items: []types.ProductListItem{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 4. 统计每个产品关联功能的成本价之和
|
||||
// 4.1 收集产品ID列表
|
||||
productIdList := make([]string, 0, len(productList))
|
||||
for _, p := range productList {
|
||||
productIdList = append(productIdList, p.Id)
|
||||
}
|
||||
|
||||
// 4.2 查询 product_feature 记录
|
||||
productFeatureBuilder := l.svcCtx.ProductFeatureModel.SelectBuilder().
|
||||
Where(squirrel.Eq{"product_id": productIdList})
|
||||
productFeatureList, err := l.svcCtx.ProductFeatureModel.FindAll(l.ctx, productFeatureBuilder, "")
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
|
||||
"查询产品功能关联失败, err: %v, productIds: %+v", err, productIdList)
|
||||
}
|
||||
|
||||
// 4.3 没有关联功能时直接返回(成本置 0)
|
||||
if len(productFeatureList) == 0 {
|
||||
items := make([]types.ProductListItem, 0, len(productList))
|
||||
for _, item := range productList {
|
||||
listItem := types.ProductListItem{
|
||||
Id: item.Id,
|
||||
ProductName: item.ProductName,
|
||||
ProductEn: item.ProductEn,
|
||||
Description: item.Description,
|
||||
Notes: item.Notes.String,
|
||||
SellPrice: item.SellPrice,
|
||||
CostPrice: 0,
|
||||
CreateTime: item.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
UpdateTime: item.UpdateTime.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
items = append(items, listItem)
|
||||
}
|
||||
|
||||
return &types.AdminGetProductListResp{
|
||||
Total: total,
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 4.4 收集 featureId 列表
|
||||
featureIdList := make([]string, 0, len(productFeatureList))
|
||||
for _, pf := range productFeatureList {
|
||||
featureIdList = append(featureIdList, pf.FeatureId)
|
||||
}
|
||||
|
||||
// 4.5 查询功能表,取出每个功能的成本价
|
||||
featureBuilder := l.svcCtx.FeatureModel.SelectBuilder().
|
||||
Where(squirrel.Eq{"id": featureIdList})
|
||||
featureList, err := l.svcCtx.FeatureModel.FindAll(l.ctx, featureBuilder, "")
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
|
||||
"查询功能成本价失败, err: %v, featureIds: %+v", err, featureIdList)
|
||||
}
|
||||
|
||||
// 4.6 构建 featureId -> cost_price 映射
|
||||
featureCostMap := make(map[string]float64, len(featureList))
|
||||
for _, f := range featureList {
|
||||
featureCostMap[f.Id] = f.CostPrice
|
||||
}
|
||||
|
||||
// 4.7 计算每个产品的成本价(所有关联 feature 的 cost_price 之和)
|
||||
productCostMap := make(map[string]float64, len(productList))
|
||||
for _, pf := range productFeatureList {
|
||||
cost, ok := featureCostMap[pf.FeatureId]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
productCostMap[pf.ProductId] += cost
|
||||
}
|
||||
|
||||
// 5. 构建响应列表
|
||||
items := make([]types.ProductListItem, 0, len(productList))
|
||||
for _, item := range productList {
|
||||
listItem := types.ProductListItem{
|
||||
Id: item.Id,
|
||||
ProductName: item.ProductName,
|
||||
ProductEn: item.ProductEn,
|
||||
Description: item.Description,
|
||||
Notes: item.Notes.String,
|
||||
CostPrice: item.CostPrice,
|
||||
SellPrice: item.SellPrice,
|
||||
CostPrice: productCostMap[item.Id],
|
||||
CreateTime: item.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
UpdateTime: item.UpdateTime.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
items = append(items, listItem)
|
||||
}
|
||||
|
||||
// 5. 返回结果
|
||||
// 6. 返回结果
|
||||
return &types.AdminGetProductListResp{
|
||||
Total: total,
|
||||
Items: items,
|
||||
|
||||
@@ -2,10 +2,14 @@ package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"bdqr-server/app/main/api/internal/svc"
|
||||
"bdqr-server/app/main/api/internal/types"
|
||||
"bdqr-server/common/xerr"
|
||||
"bdqr-server/app/main/model"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
@@ -24,7 +28,61 @@ func NewCheckFeatureWhitelistStatusLogic(ctx context.Context, svcCtx *svc.Servic
|
||||
}
|
||||
|
||||
func (l *CheckFeatureWhitelistStatusLogic) CheckFeatureWhitelistStatus(req *types.CheckFeatureWhitelistStatusReq) (resp *types.CheckFeatureWhitelistStatusResp, err error) {
|
||||
// todo: add your logic here and delete this line
|
||||
// 1. 验证参数
|
||||
if req.IdCard == "" {
|
||||
return nil, errors.Wrapf(xerr.NewErrMsg("身份证号不能为空"), "")
|
||||
}
|
||||
if req.FeatureApiId == "" {
|
||||
return nil, errors.Wrapf(xerr.NewErrMsg("模块API标识不能为空"), "")
|
||||
}
|
||||
|
||||
return
|
||||
// 2. 提取主模块ID(去掉下划线后的部分)
|
||||
mainApiId := req.FeatureApiId
|
||||
if idx := strings.Index(req.FeatureApiId, "_"); idx > 0 {
|
||||
mainApiId = req.FeatureApiId[:idx]
|
||||
}
|
||||
|
||||
// 3. 查询feature信息(使用主模块ID)
|
||||
feature, err := l.svcCtx.FeatureModel.FindOneByApiId(l.ctx, mainApiId)
|
||||
if err != nil {
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return nil, errors.Wrapf(xerr.NewErrMsg("模块不存在"), "")
|
||||
}
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询模块信息失败, %v", err)
|
||||
}
|
||||
|
||||
// 4. 检查是否在白名单中(使用主模块ID)
|
||||
whitelistBuilder := l.svcCtx.UserFeatureWhitelistModel.SelectBuilder().
|
||||
Where("id_card = ? AND feature_api_id = ? AND status = ?", req.IdCard, mainApiId, 1)
|
||||
whitelists, err := l.svcCtx.UserFeatureWhitelistModel.FindAll(l.ctx, whitelistBuilder, "")
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询白名单记录失败, %v", err)
|
||||
}
|
||||
|
||||
isWhitelisted := len(whitelists) > 0
|
||||
|
||||
// 5. 如果提供了 queryId,检查当前报告数据是否已删除
|
||||
dataDeleted := false
|
||||
if req.QueryId != "" {
|
||||
containsFeature, err := l.svcCtx.WhitelistService.CheckQueryDataContainsFeature(l.ctx, req.QueryId, req.FeatureApiId)
|
||||
if err != nil {
|
||||
// 检查失败不影响主流程,记录日志即可
|
||||
logx.Errorf("检查报告数据是否包含模块失败:查询记录 %s,模块 %s,错误:%v", req.QueryId, req.FeatureApiId, err)
|
||||
// 默认认为数据已删除(保守处理)
|
||||
dataDeleted = true
|
||||
} else {
|
||||
// 如果数据中不包含该模块,说明已删除
|
||||
dataDeleted = !containsFeature
|
||||
}
|
||||
} else {
|
||||
// 未提供 queryId,无法判断数据是否已删除,默认认为已删除(保守处理)
|
||||
dataDeleted = true
|
||||
}
|
||||
|
||||
return &types.CheckFeatureWhitelistStatusResp{
|
||||
IsWhitelisted: isWhitelisted,
|
||||
WhitelistPrice: feature.WhitelistPrice,
|
||||
FeatureId: feature.Id,
|
||||
DataDeleted: dataDeleted,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -2,11 +2,18 @@ package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"bdqr-server/app/main/api/internal/svc"
|
||||
"bdqr-server/app/main/api/internal/types"
|
||||
"bdqr-server/app/main/model"
|
||||
"bdqr-server/common/ctxdata"
|
||||
"bdqr-server/common/xerr"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"github.com/zeromicro/go-zero/core/stores/sqlx"
|
||||
)
|
||||
|
||||
type CreateWhitelistOrderLogic struct {
|
||||
@@ -24,7 +31,132 @@ func NewCreateWhitelistOrderLogic(ctx context.Context, svcCtx *svc.ServiceContex
|
||||
}
|
||||
|
||||
func (l *CreateWhitelistOrderLogic) CreateWhitelistOrder(req *types.CreateWhitelistOrderReq) (resp *types.CreateWhitelistOrderResp, err error) {
|
||||
// todo: add your logic here and delete this line
|
||||
userID, err := ctxdata.GetUidFromCtx(l.ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
// 1. 获取代理信息并验证(任意等级代理均可操作白名单)
|
||||
_, err = l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
|
||||
}
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
|
||||
}
|
||||
|
||||
// 2. 验证参数
|
||||
if req.IdCard == "" {
|
||||
return nil, errors.Wrapf(xerr.NewErrMsg("身份证号不能为空"), "")
|
||||
}
|
||||
if len(req.FeatureIds) == 0 {
|
||||
return nil, errors.Wrapf(xerr.NewErrMsg("请至少选择一个模块"), "")
|
||||
}
|
||||
|
||||
// 3. 查询feature信息并计算总金额
|
||||
var totalAmount float64
|
||||
var orderItems []struct {
|
||||
FeatureId string
|
||||
FeatureApiId string
|
||||
FeatureName string
|
||||
Price float64
|
||||
}
|
||||
|
||||
for _, featureId := range req.FeatureIds {
|
||||
feature, err := l.svcCtx.FeatureModel.FindOne(l.ctx, featureId)
|
||||
if err != nil {
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return nil, errors.Wrapf(xerr.NewErrMsg(fmt.Sprintf("模块不存在: %s", featureId)), "")
|
||||
}
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询模块信息失败, %v", err)
|
||||
}
|
||||
|
||||
// 直接使用feature的WhitelistPrice字段
|
||||
whitelistPrice := feature.WhitelistPrice
|
||||
|
||||
if whitelistPrice <= 0 {
|
||||
return nil, errors.Wrapf(xerr.NewErrMsg(fmt.Sprintf("模块 %s 不支持白名单屏蔽", feature.Name)), "")
|
||||
}
|
||||
|
||||
// 检查该身份证号+feature是否已经存在白名单记录
|
||||
whitelistBuilder := l.svcCtx.UserFeatureWhitelistModel.SelectBuilder().
|
||||
Where("id_card = ? AND feature_id = ?", req.IdCard, featureId)
|
||||
existing, err := l.svcCtx.UserFeatureWhitelistModel.FindAll(l.ctx, whitelistBuilder, "")
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询白名单记录失败, %v", err)
|
||||
}
|
||||
// 检查是否有生效的记录(status=1)
|
||||
for _, item := range existing {
|
||||
if item.Status == 1 {
|
||||
return nil, errors.Wrapf(xerr.NewErrMsg(fmt.Sprintf("身份证号 %s 的模块 %s 已经加入白名单", req.IdCard, feature.Name)), "")
|
||||
}
|
||||
}
|
||||
|
||||
totalAmount += whitelistPrice
|
||||
orderItems = append(orderItems, struct {
|
||||
FeatureId string
|
||||
FeatureApiId string
|
||||
FeatureName string
|
||||
Price float64
|
||||
}{
|
||||
FeatureId: feature.Id,
|
||||
FeatureApiId: feature.ApiId,
|
||||
FeatureName: feature.Name,
|
||||
Price: whitelistPrice,
|
||||
})
|
||||
}
|
||||
|
||||
// 4. 生成订单号(白名单订单前缀 W_,限制长度不超过32)
|
||||
base := l.svcCtx.AlipayService.GenerateOutTradeNo()
|
||||
orderNo := "W_" + base
|
||||
if len(orderNo) > 32 {
|
||||
orderNo = orderNo[:32]
|
||||
}
|
||||
|
||||
// 5. 使用事务创建订单和订单明细
|
||||
var orderId string
|
||||
err = l.svcCtx.WhitelistOrderModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error {
|
||||
// 创建订单
|
||||
order := &model.WhitelistOrder{
|
||||
Id: uuid.NewString(),
|
||||
OrderNo: orderNo,
|
||||
UserId: userID,
|
||||
IdCard: req.IdCard,
|
||||
TotalAmount: totalAmount,
|
||||
Status: 1, // 待支付
|
||||
}
|
||||
_, err := l.svcCtx.WhitelistOrderModel.Insert(ctx, session, order)
|
||||
if err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建订单失败, %v", err)
|
||||
}
|
||||
orderId = order.Id
|
||||
|
||||
// 创建订单明细
|
||||
for _, item := range orderItems {
|
||||
orderItem := &model.WhitelistOrderItem{
|
||||
Id: uuid.NewString(),
|
||||
OrderId: orderId,
|
||||
FeatureId: item.FeatureId,
|
||||
FeatureApiId: item.FeatureApiId,
|
||||
FeatureName: item.FeatureName,
|
||||
Price: item.Price,
|
||||
}
|
||||
_, err := l.svcCtx.WhitelistOrderItemModel.Insert(ctx, session, orderItem)
|
||||
if err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建订单明细失败, %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.CreateWhitelistOrderResp{
|
||||
OrderId: orderId,
|
||||
OrderNo: orderNo,
|
||||
TotalAmount: totalAmount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package agent
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"bdqr-server/app/main/model"
|
||||
@@ -52,13 +53,20 @@ func (l *GetInvitePosterLogic) GetInvitePoster(req *types.GetInvitePosterReq) (r
|
||||
return nil, errors.Wrapf(xerr.NewErrMsg("邀请链接不能为空"), "")
|
||||
}
|
||||
|
||||
// 3. 调用 ImageService 生成邀请海报(背景图 + 二维码)
|
||||
imgData, _, err := l.svcCtx.ImageService.ProcessImageWithQRCode("invitation", inviteLink)
|
||||
// 3. 确定模板类型(1-4 对应 invitation_01 ~ invitation_04)
|
||||
templateIndex := req.TemplateIndex
|
||||
if templateIndex < 1 || templateIndex > 4 {
|
||||
templateIndex = 1
|
||||
}
|
||||
qrcodeType := fmt.Sprintf("invitation_%02d", templateIndex)
|
||||
|
||||
// 4. 调用 ImageService 生成邀请海报(背景图 + 二维码)
|
||||
imgData, _, err := l.svcCtx.ImageService.ProcessImageWithQRCode(qrcodeType, inviteLink)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成海报失败: %v", err)
|
||||
}
|
||||
|
||||
// 4. 返回 base64
|
||||
// 5. 返回 base64
|
||||
posterBase64 := base64.StdEncoding.EncodeToString(imgData)
|
||||
return &types.GetInvitePosterResp{
|
||||
PosterBase64: posterBase64,
|
||||
|
||||
@@ -6,6 +6,11 @@ import (
|
||||
"bdqr-server/common/globalkey"
|
||||
"bdqr-server/common/xerr"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"bdqr-server/pkg/lzkit/crypto"
|
||||
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/pkg/errors"
|
||||
@@ -47,7 +52,7 @@ func (l *GetPromotionQueryListLogic) GetPromotionQueryList(req *types.GetPromoti
|
||||
builder := l.svcCtx.AgentOrderModel.SelectBuilder().
|
||||
Where("agent_id = ? AND del_state = ?", agent.Id, globalkey.DelStateNo)
|
||||
|
||||
orders, total, err := l.svcCtx.AgentOrderModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC")
|
||||
orders, _, err := l.svcCtx.AgentOrderModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC")
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理订单失败, %v", err)
|
||||
}
|
||||
@@ -71,6 +76,34 @@ func (l *GetPromotionQueryListLogic) GetPromotionQueryList(req *types.GetPromoti
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询产品信息失败, %v", pErr)
|
||||
}
|
||||
|
||||
// 获取订单价格
|
||||
order, oErr := l.svcCtx.OrderModel.FindOne(l.ctx, ao.OrderId)
|
||||
if oErr != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单信息失败, %v", oErr)
|
||||
}
|
||||
|
||||
// 解密并脱敏params
|
||||
var params map[string]interface{}
|
||||
if q.QueryParams != "" {
|
||||
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", decodeErr)
|
||||
}
|
||||
decryptedData, decryptErr := crypto.AesDecrypt(q.QueryParams, key)
|
||||
if decryptErr != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "解密查询参数失败, %v", decryptErr)
|
||||
}
|
||||
unmarshalErr := json.Unmarshal(decryptedData, ¶ms)
|
||||
if unmarshalErr != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "解析查询参数失败, %v", unmarshalErr)
|
||||
}
|
||||
// 脱敏处理
|
||||
params = l.desensitizeParams(params)
|
||||
} else {
|
||||
params = make(map[string]interface{})
|
||||
}
|
||||
|
||||
item := types.PromotionQueryItem{}
|
||||
_ = copier.Copy(&item, q)
|
||||
item.Id = q.Id
|
||||
@@ -78,11 +111,74 @@ func (l *GetPromotionQueryListLogic) GetPromotionQueryList(req *types.GetPromoti
|
||||
item.ProductName = product.ProductName
|
||||
item.CreateTime = q.CreateTime.Format("2006-01-02 15:04:05")
|
||||
item.QueryState = q.QueryState
|
||||
item.Params = params
|
||||
item.Price = order.Amount
|
||||
list = append(list, item)
|
||||
}
|
||||
|
||||
return &types.GetPromotionQueryListResp{
|
||||
Total: total, // 前端仅展示已创建查询条目
|
||||
Total: int64(len(list)), // 仅统计已创建查询的条目
|
||||
List: list,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// desensitizeParams 对敏感数据进行脱敏处理
|
||||
func (l *GetPromotionQueryListLogic) desensitizeParams(params map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
for key, value := range params {
|
||||
if strValue, ok := value.(string); ok {
|
||||
keyLower := strings.ToLower(key)
|
||||
if (strings.Contains(keyLower, "name") || strings.Contains(keyLower, "姓名")) && len(strValue) > 0 {
|
||||
result[key] = l.maskName(strValue)
|
||||
} else if (strings.Contains(keyLower, "idcard") || strings.Contains(keyLower, "id_card") || strings.Contains(keyLower, "身份证")) && len(strValue) > 10 {
|
||||
result[key] = l.maskIDCard(strValue)
|
||||
} else if (strings.Contains(keyLower, "mobile") || strings.Contains(keyLower, "phone") || strings.Contains(keyLower, "手机")) && len(strValue) >= 8 {
|
||||
result[key] = l.maskPhone(strValue)
|
||||
} else {
|
||||
result[key] = strValue
|
||||
}
|
||||
} else {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// maskName 姓名脱敏
|
||||
func (l *GetPromotionQueryListLogic) maskName(name string) string {
|
||||
runes := []rune(name)
|
||||
length := len(runes)
|
||||
if length <= 1 {
|
||||
return name
|
||||
}
|
||||
if length == 2 {
|
||||
return string(runes[0]) + "*"
|
||||
}
|
||||
return string(runes[0]) + strings.Repeat("*", length-2) + string(runes[length-1])
|
||||
}
|
||||
|
||||
// maskIDCard 身份证号脱敏
|
||||
func (l *GetPromotionQueryListLogic) maskIDCard(idCard string) string {
|
||||
length := len(idCard)
|
||||
if length <= 10 {
|
||||
return idCard
|
||||
}
|
||||
// 保留前3位和后4位
|
||||
if length > 7 {
|
||||
return idCard[:3] + strings.Repeat("*", length-7) + idCard[length-4:]
|
||||
}
|
||||
return idCard
|
||||
}
|
||||
|
||||
// maskPhone 手机号脱敏
|
||||
func (l *GetPromotionQueryListLogic) maskPhone(phone string) string {
|
||||
length := len(phone)
|
||||
if length < 8 {
|
||||
return phone
|
||||
}
|
||||
// 保留前3位和后4位
|
||||
if length > 7 {
|
||||
return phone[:3] + strings.Repeat("*", length-7) + phone[length-4:]
|
||||
}
|
||||
return phone
|
||||
}
|
||||
|
||||
@@ -38,15 +38,23 @@ func NewRegisterByInviteCodeLogic(ctx context.Context, svcCtx *svc.ServiceContex
|
||||
}
|
||||
}
|
||||
|
||||
// truncateAuthKey 截断 authKey 用于日志,避免敏感信息过长
|
||||
func truncateAuthKey(s string) string {
|
||||
if len(s) <= 12 {
|
||||
return s
|
||||
}
|
||||
return s[:6] + "..." + s[len(s)-4:]
|
||||
}
|
||||
|
||||
func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByInviteCodeReq) (resp *types.RegisterByInviteCodeResp, err error) {
|
||||
l.Infof("[RegisterByInviteCode] 开始处理代理注册请求, mobile: %s, referrer: %s", req.Mobile, req.Referrer)
|
||||
l.Infof("[RegisterByInviteCode] 开始处理代理注册请求 | mobile: %s, referrer: %s, code: %s", req.Mobile, req.Referrer, req.Code)
|
||||
|
||||
secretKey := l.svcCtx.Config.Encrypt.SecretKey
|
||||
encryptedMobile, err := crypto.EncryptMobile(req.Mobile, secretKey)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败: %v", err)
|
||||
}
|
||||
l.Infof("[RegisterByInviteCode] 手机号加密完成, encryptedMobile: %s", encryptedMobile)
|
||||
l.Infof("[RegisterByInviteCode] 手机号加密完成 | mobile: %s, encryptedMobile: %s", req.Mobile, encryptedMobile)
|
||||
|
||||
// 校验验证码(开发环境下跳过验证码校验)
|
||||
if os.Getenv("ENV") != "development" && req.Code != "143838" {
|
||||
@@ -76,9 +84,9 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败: %v", err)
|
||||
}
|
||||
if claims != nil {
|
||||
l.Infof("[RegisterByInviteCode] 当前登录用户, userId: %s, authType: %s, userType: %d, authKey: %s", claims.UserId, claims.AuthType, claims.UserType, claims.AuthKey)
|
||||
l.Infof("[RegisterByInviteCode] 当前登录态 | userId: %s, authType: %s, userType: %d, authKeyPrefix: %s", claims.UserId, claims.AuthType, claims.UserType, truncateAuthKey(claims.AuthKey))
|
||||
} else {
|
||||
l.Infof("[RegisterByInviteCode] 未登录状态(claims为nil,将按未登录流程处理)")
|
||||
l.Infof("[RegisterByInviteCode] 未登录状态(claims 为 nil,将按未登录流程处理)")
|
||||
}
|
||||
|
||||
// 前置检查:如果当前用户是正式用户(有手机号),进行拦截检查
|
||||
@@ -88,24 +96,25 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询用户失败: %v", err)
|
||||
}
|
||||
if currentUser != nil && currentUser.Mobile.Valid && currentUser.Mobile.String != "" {
|
||||
l.Infof("[RegisterByInviteCode] 当前用户是正式用户, userId: %s, mobile: %s", claims.UserId, currentUser.Mobile.String)
|
||||
l.Infof("[RegisterByInviteCode] 前置检查-正式用户 | userId: %s, user.mobile: %s, 请求加密手机号: %s, 匹配: %v", claims.UserId, currentUser.Mobile.String, encryptedMobile, currentUser.Mobile.String == encryptedMobile)
|
||||
// 当前用户是正式用户,检查是否已是代理
|
||||
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, claims.UserId)
|
||||
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理失败: %v", err)
|
||||
}
|
||||
if agent != nil {
|
||||
l.Infof("[RegisterByInviteCode] 用户已是代理, userId: %s, agentId: %s", claims.UserId, agent.Id)
|
||||
l.Infof("[RegisterByInviteCode] 前置检查-已是代理拒绝 | userId: %s, agentId: %s", claims.UserId, agent.Id)
|
||||
return nil, errors.Wrapf(xerr.NewErrMsg("您已经是代理,不能重复注册"), "")
|
||||
}
|
||||
// 正式用户手机号必须匹配
|
||||
if currentUser.Mobile.String != encryptedMobile {
|
||||
l.Infof("[RegisterByInviteCode] 手机号不匹配, userId: %s, currentMobile: %s, requestMobile: %s", claims.UserId, currentUser.Mobile.String, encryptedMobile)
|
||||
l.Infof("[RegisterByInviteCode] 前置检查-手机号不匹配拒绝 | userId: %s, currentMobile: %s, requestEncrypted: %s", claims.UserId, currentUser.Mobile.String, encryptedMobile)
|
||||
return nil, errors.Wrapf(xerr.NewErrMsg("请输入当前账号的手机号码"), "")
|
||||
}
|
||||
l.Infof("[RegisterByInviteCode] 前置检查通过, 正式用户手机号匹配")
|
||||
l.Infof("[RegisterByInviteCode] 前置检查通过 | 正式用户且手机号匹配,非代理")
|
||||
} else {
|
||||
l.Infof("[RegisterByInviteCode] 当前用户是临时用户或无手机号, userId: %s", claims.UserId)
|
||||
hasMobile := currentUser != nil && currentUser.Mobile.Valid && currentUser.Mobile.String != ""
|
||||
l.Infof("[RegisterByInviteCode] 前置检查-临时用户或无手机号 | userId: %s, userHasMobile: %v", claims.UserId, hasMobile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,16 +150,16 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
|
||||
|
||||
l.Infof("[RegisterByInviteCode] 开始事务处理")
|
||||
err = l.svcCtx.AgentInviteCodeModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error {
|
||||
// 1. 查找目标用户(通过手机号)
|
||||
// 1. 查找目标用户(通过手机号,可能命中缓存)
|
||||
targetUser, findUserErr := l.svcCtx.UserModel.FindOneByMobile(transCtx, sql.NullString{String: encryptedMobile, Valid: true})
|
||||
if findUserErr != nil && !errors.Is(findUserErr, model.ErrNotFound) {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询用户失败, %v", findUserErr)
|
||||
}
|
||||
|
||||
if targetUser != nil {
|
||||
l.Infof("[RegisterByInviteCode] 找到目标用户, userId: %s", targetUser.Id)
|
||||
l.Infof("[RegisterByInviteCode] FindOneByMobile 命中 | targetUserId: %s, encryptedMobile: %s | 走场景2(手机号已存在)", targetUser.Id, encryptedMobile)
|
||||
} else {
|
||||
l.Infof("[RegisterByInviteCode] 目标用户不存在, 将创建新用户")
|
||||
l.Infof("[RegisterByInviteCode] FindOneByMobile 未命中 | targetUser=nil, encryptedMobile: %s | 走场景1(手机号不存在)。若用户刚执行过 bindMobile 仍为 nil,疑为 FindOneByMobile 缓存未失效", encryptedMobile)
|
||||
}
|
||||
|
||||
// 2. 获取当前登录态信息
|
||||
@@ -168,21 +177,21 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
|
||||
|
||||
// 3. 根据目标用户是否存在,处理用户和认证
|
||||
if targetUser == nil {
|
||||
// 场景1: 手机号不存在
|
||||
l.Infof("[RegisterByInviteCode] 场景1: 手机号不存在, currentUserID: %s", currentUserID)
|
||||
l.Infof("[RegisterByInviteCode] 分支: 场景1-手机号不存在 | currentUserID: %s, encryptedMobile: %s", currentUserID, encryptedMobile)
|
||||
userID, err = l.handleMobileNotExists(transCtx, session, encryptedMobile, currentUserID)
|
||||
if err != nil {
|
||||
l.Errorf("[RegisterByInviteCode] 场景1失败 | currentUserID: %s, encryptedMobile: %s, err: %v", currentUserID, encryptedMobile, err)
|
||||
return err
|
||||
}
|
||||
l.Infof("[RegisterByInviteCode] 场景1处理完成, userID: %s", userID)
|
||||
l.Infof("[RegisterByInviteCode] 场景1完成 | userID: %s", userID)
|
||||
} else {
|
||||
// 场景2: 手机号已存在
|
||||
l.Infof("[RegisterByInviteCode] 场景2: 手机号已存在, targetUserId: %s, currentUserID: %s", targetUser.Id, currentUserID)
|
||||
l.Infof("[RegisterByInviteCode] 分支: 场景2-手机号已存在 | targetUserId: %s, currentUserID: %s, sameUser: %v", targetUser.Id, currentUserID, targetUser.Id == currentUserID)
|
||||
userID, err = l.handleMobileExists(transCtx, session, targetUser, currentUserID, currentAuthType, currentAuthKey)
|
||||
if err != nil {
|
||||
l.Errorf("[RegisterByInviteCode] 场景2失败 | targetUserId: %s, currentUserID: %s, err: %v", targetUser.Id, currentUserID, err)
|
||||
return err
|
||||
}
|
||||
l.Infof("[RegisterByInviteCode] 场景2处理完成, userID: %s", userID)
|
||||
l.Infof("[RegisterByInviteCode] 场景2完成 | userID: %s", userID)
|
||||
}
|
||||
// 4. 处理邀请码和上级关系
|
||||
var targetLevel int64
|
||||
@@ -342,7 +351,7 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
l.Errorf("[RegisterByInviteCode] 事务处理失败: %v", err)
|
||||
l.Errorf("[RegisterByInviteCode] 事务失败 | mobile: %s, referrer: %s, err: %v | 排查可看上文 FindOneByMobile 是否命中、场景1/2 及 handleMobileNotExists/Exists 日志", req.Mobile, req.Referrer, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -464,82 +473,74 @@ func (l *RegisterByInviteCodeLogic) allocateAgentCode(ctx context.Context, sessi
|
||||
// handleMobileNotExists 处理手机号不存在的情况
|
||||
func (l *RegisterByInviteCodeLogic) handleMobileNotExists(ctx context.Context, session sqlx.Session, encryptedMobile string, currentUserID string) (string, error) {
|
||||
if currentUserID == "" {
|
||||
// 场景1.1: 未登录 + 手机号不存在 -> 创建新用户
|
||||
l.Infof("[handleMobileNotExists] 场景1.1: 未登录+手机号不存在, 创建新用户")
|
||||
l.Infof("[handleMobileNotExists] 子分支 1.1: 未登录+手机号不存在 | 创建新用户, encryptedMobile: %s", encryptedMobile)
|
||||
newUser := &model.User{Id: uuid.NewString(), Mobile: sql.NullString{String: encryptedMobile, Valid: true}}
|
||||
if _, err := l.svcCtx.UserModel.Insert(ctx, session, newUser); err != nil {
|
||||
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户失败: %v", err)
|
||||
}
|
||||
l.Infof("[handleMobileNotExists] 用户创建成功, userId: %s", newUser.Id)
|
||||
// 创建 mobile 认证
|
||||
l.Infof("[handleMobileNotExists] 1.1 用户已创建 | userId: %s", newUser.Id)
|
||||
l.Infof("[handleMobileNotExists] 1.1 即将插入 user_auth(mobile) | userId: %s, authKey: %s", newUser.Id, encryptedMobile)
|
||||
if _, err := l.svcCtx.UserAuthModel.Insert(ctx, session, &model.UserAuth{
|
||||
Id: uuid.NewString(),
|
||||
UserId: newUser.Id,
|
||||
AuthType: model.UserAuthTypeMobile,
|
||||
AuthKey: encryptedMobile,
|
||||
}); err != nil {
|
||||
l.Errorf("[handleMobileNotExists] 1.1 插入 user_auth 失败(可能 Duplicate) | userId: %s, err: %v", newUser.Id, err)
|
||||
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建手机号认证失败: %v", err)
|
||||
}
|
||||
l.Infof("[handleMobileNotExists] 手机号认证创建成功, userId: %s", newUser.Id)
|
||||
l.Infof("[handleMobileNotExists] 1.1 完成 | userId: %s", newUser.Id)
|
||||
return newUser.Id, nil
|
||||
} else {
|
||||
// 场景1.2: 已登录临时用户 + 手机号不存在 -> 升级为正式用户
|
||||
// 前置检查已保证不是正式用户,所以这里一定是临时用户
|
||||
l.Infof("[handleMobileNotExists] 场景1.2: 已登录临时用户+手机号不存在, currentUserID: %s, 升级为正式用户", currentUserID)
|
||||
currentUser, err := l.svcCtx.UserModel.FindOne(ctx, currentUserID)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询当前用户失败: %v", err)
|
||||
}
|
||||
// 升级为正式用户
|
||||
currentUser.Mobile = sql.NullString{String: encryptedMobile, Valid: true}
|
||||
if _, err := l.svcCtx.UserModel.Update(ctx, session, currentUser); err != nil {
|
||||
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新手机号失败: %v", err)
|
||||
}
|
||||
l.Infof("[handleMobileNotExists] 用户升级为正式用户成功, userId: %s", currentUserID)
|
||||
// 创建 mobile 认证
|
||||
if _, err := l.svcCtx.UserAuthModel.Insert(ctx, session, &model.UserAuth{
|
||||
Id: uuid.NewString(),
|
||||
UserId: currentUserID,
|
||||
AuthType: model.UserAuthTypeMobile,
|
||||
AuthKey: encryptedMobile,
|
||||
}); err != nil {
|
||||
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建手机号认证失败: %v", err)
|
||||
}
|
||||
l.Infof("[handleMobileNotExists] 手机号认证创建成功, userId: %s", currentUserID)
|
||||
return currentUserID, nil
|
||||
}
|
||||
// 场景1.2: 已登录临时用户 + 手机号不存在 -> 升级为正式用户
|
||||
l.Infof("[handleMobileNotExists] 子分支 1.2: 已登录+手机号不存在 | currentUserID: %s, encryptedMobile: %s, 将更新 user.mobile 并插入 user_auth", currentUserID, encryptedMobile)
|
||||
currentUser, err := l.svcCtx.UserModel.FindOne(ctx, currentUserID)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询当前用户失败: %v", err)
|
||||
}
|
||||
currentUser.Mobile = sql.NullString{String: encryptedMobile, Valid: true}
|
||||
if _, err := l.svcCtx.UserModel.Update(ctx, session, currentUser); err != nil {
|
||||
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新手机号失败: %v", err)
|
||||
}
|
||||
l.Infof("[handleMobileNotExists] 1.2 user.mobile 已更新 | userId: %s", currentUserID)
|
||||
l.Infof("[handleMobileNotExists] 1.2 即将插入 user_auth(mobile) | userId: %s, authKey: %s | 若报 Duplicate 说明该手机号已绑定,疑缓存导致误走场景1", currentUserID, encryptedMobile)
|
||||
if _, err := l.svcCtx.UserAuthModel.Insert(ctx, session, &model.UserAuth{
|
||||
Id: uuid.NewString(),
|
||||
UserId: currentUserID,
|
||||
AuthType: model.UserAuthTypeMobile,
|
||||
AuthKey: encryptedMobile,
|
||||
}); err != nil {
|
||||
l.Errorf("[handleMobileNotExists] 1.2 插入 user_auth 失败 | userId: %s, err: %v | 常见为 Duplicate entry 'mobile-xxx',多为 FindOneByMobile 缓存未失效", currentUserID, err)
|
||||
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建手机号认证失败: %v", err)
|
||||
}
|
||||
l.Infof("[handleMobileNotExists] 1.2 完成 | userId: %s", currentUserID)
|
||||
return currentUserID, nil
|
||||
}
|
||||
|
||||
// handleMobileExists 处理手机号已存在的情况
|
||||
func (l *RegisterByInviteCodeLogic) handleMobileExists(ctx context.Context, session sqlx.Session, targetUser *model.User, currentUserID string, currentAuthType string, currentAuthKey string) (string, error) {
|
||||
userID := targetUser.Id
|
||||
l.Infof("[handleMobileExists] 开始处理, targetUserId: %s, currentUserID: %s", userID, currentUserID)
|
||||
l.Infof("[handleMobileExists] 入口 | targetUserId: %s, currentUserID: %s, authType: %s", userID, currentUserID, currentAuthType)
|
||||
|
||||
// 检查目标用户是否已是代理
|
||||
existingAgent, err := l.svcCtx.AgentModel.FindOneByUserId(ctx, userID)
|
||||
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
||||
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败: %v", err)
|
||||
}
|
||||
if existingAgent != nil {
|
||||
l.Infof("[handleMobileExists] 目标用户已是代理, userId: %s, agentId: %s", userID, existingAgent.Id)
|
||||
l.Infof("[handleMobileExists] 目标已是代理拒绝 | userId: %s, agentId: %s", userID, existingAgent.Id)
|
||||
return "", errors.Wrapf(xerr.NewErrMsg("该手机号已经是代理,不能重复注册"), "")
|
||||
}
|
||||
|
||||
if currentUserID == "" {
|
||||
// 场景2.1: 未登录 + 手机号存在 -> 直接使用目标用户(验证码已确认身份)
|
||||
l.Infof("[handleMobileExists] 场景2.1: 未登录+手机号存在, 直接使用目标用户, userId: %s", userID)
|
||||
l.Infof("[handleMobileExists] 子分支 2.1: 未登录+手机号存在 | 直接使用 targetUserId: %s", userID)
|
||||
return userID, nil
|
||||
} else if currentUserID == userID {
|
||||
// 场景2.2: 已登录正式用户 + 手机号匹配 -> 直接使用
|
||||
// 前置检查已保证手机号匹配且不是代理
|
||||
l.Infof("[handleMobileExists] 场景2.2: 已登录正式用户+手机号匹配, userId: %s", userID)
|
||||
return userID, nil
|
||||
} else {
|
||||
// 场景2.3: 已登录临时用户 + 手机号存在 -> 需要合并账号
|
||||
// 前置检查已保证是临时用户(不是正式用户)
|
||||
l.Infof("[handleMobileExists] 场景2.3: 已登录临时用户+手机号存在, 需要合并账号, sourceUserId: %s, targetUserId: %s, authType: %s", currentUserID, userID, currentAuthType)
|
||||
return l.mergeTempUserToTarget(ctx, session, currentUserID, userID, currentAuthType, currentAuthKey)
|
||||
}
|
||||
if currentUserID == userID {
|
||||
l.Infof("[handleMobileExists] 子分支 2.2: 已登录且与目标同一用户 | userId: %s", userID)
|
||||
return userID, nil
|
||||
}
|
||||
l.Infof("[handleMobileExists] 子分支 2.3: 已登录临时用户+手机号属他人 | 合并 source: %s -> target: %s, authType: %s", currentUserID, userID, currentAuthType)
|
||||
return l.mergeTempUserToTarget(ctx, session, currentUserID, userID, currentAuthType, currentAuthKey)
|
||||
}
|
||||
|
||||
// mergeTempUserToTarget 合并临时用户到目标用户
|
||||
|
||||
@@ -36,24 +36,26 @@ func NewBindMobileLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindMo
|
||||
}
|
||||
|
||||
func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.BindMobileResp, err error) {
|
||||
// 从上下文中获取当前登录态的用户声明(可能是临时用户或正式用户),包含UserId/AuthType/AuthKey
|
||||
l.Infof("[BindMobile] 开始 | mobile: %s", req.Mobile)
|
||||
|
||||
claims, err := ctxdata.GetClaimsFromCtx(l.ctx)
|
||||
if err != nil && !errors.Is(err, ctxdata.ErrNoInCtx) {
|
||||
l.Errorf("[BindMobile] 获取登录态失败: %v", err)
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败: %v", err)
|
||||
}
|
||||
|
||||
// 当前登录用户信息(用于后续合并/绑定)
|
||||
currentUserID := claims.UserId
|
||||
currentAuthType := claims.AuthType
|
||||
currentAuthKey := claims.AuthKey
|
||||
l.Infof("[BindMobile] 当前登录态 | userId: %s, authType: %s", currentUserID, currentAuthType)
|
||||
|
||||
// 加密手机号(所有手机号以密文存储)
|
||||
secretKey := l.svcCtx.Config.Encrypt.SecretKey
|
||||
encryptedMobile, err := crypto.EncryptMobile(req.Mobile, secretKey)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败: %v", err)
|
||||
}
|
||||
// 非开发环境下校验短信验证码(从Redis读取并比对)
|
||||
l.Infof("[BindMobile] 手机号加密完成 | mobile: %s, encryptedMobile: %s", req.Mobile, encryptedMobile)
|
||||
|
||||
if os.Getenv("ENV") != "development" {
|
||||
redisKey := fmt.Sprintf("%s:%s", "bindMobile", encryptedMobile)
|
||||
cacheCode, err := l.svcCtx.Redis.Get(redisKey)
|
||||
@@ -68,56 +70,62 @@ func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.Bind
|
||||
}
|
||||
}
|
||||
|
||||
// 通过加密后的手机号查找目标用户(手机号用户视为正式用户)
|
||||
// 通过加密后的手机号查找目标用户(可能命中缓存)
|
||||
targetUser, err := l.svcCtx.UserModel.FindOneByMobile(l.ctx, sql.NullString{String: encryptedMobile, Valid: true})
|
||||
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
||||
l.Errorf("[BindMobile] FindOneByMobile 查询失败: %v", err)
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找用户失败: %v", err)
|
||||
}
|
||||
|
||||
var finalUserID string
|
||||
if targetUser == nil {
|
||||
// 手机号不存在:直接将当前用户升级为正式用户(写入mobile与mobile认证)
|
||||
finalUserID = currentUserID
|
||||
l.Infof("[BindMobile] FindOneByMobile 未命中 | targetUser=nil, encryptedMobile: %s | 分支: 手机号不存在,当前用户升级为正式用户", encryptedMobile)
|
||||
finalUserID := currentUserID
|
||||
currentUser, err := l.svcCtx.UserModel.FindOne(l.ctx, currentUserID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找当前用户失败: %v", err)
|
||||
}
|
||||
currentUser.Mobile = sql.NullString{String: encryptedMobile, Valid: true}
|
||||
if _, err := l.svcCtx.UserModel.Update(l.ctx, nil, currentUser); err != nil {
|
||||
l.Errorf("[BindMobile] 更新 user.mobile 失败 | userId: %s, err: %v", currentUserID, err)
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新手机号失败: %v", err)
|
||||
}
|
||||
// 记录mobile认证(确保后续可通过手机号登录)
|
||||
l.Infof("[BindMobile] user.mobile 已更新 | userId: %s", currentUserID)
|
||||
if _, err := l.svcCtx.UserAuthModel.Insert(l.ctx, nil, &model.UserAuth{Id: uuid.NewString(), UserId: finalUserID, AuthType: model.UserAuthTypeMobile, AuthKey: encryptedMobile}); err != nil {
|
||||
l.Errorf("[BindMobile] 插入 user_auth(mobile) 失败 | userId: %s, err: %v", finalUserID, err)
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建手机号认证失败: %v", err)
|
||||
}
|
||||
// 发放token(userType会根据mobile字段动态计算)
|
||||
l.Infof("[BindMobile] user_auth(mobile) 已插入 | userId: %s", finalUserID)
|
||||
userMobileCacheKey := fmt.Sprintf("cache:bdqr:user:mobile:%v", sql.NullString{String: encryptedMobile, Valid: true})
|
||||
_, _ = l.svcCtx.Redis.DelCtx(l.ctx, userMobileCacheKey)
|
||||
l.Infof("[BindMobile] 已失效 FindOneByMobile 缓存 | key: %s | 后续代理注册将能正确按手机号查到本用户", userMobileCacheKey)
|
||||
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成Token失败: %v", err)
|
||||
}
|
||||
now := time.Now().Unix()
|
||||
l.Infof("[BindMobile] 完成-升级正式用户 | userId: %s, mobile: %s", finalUserID, req.Mobile)
|
||||
return &types.BindMobileResp{AccessToken: token, AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire, RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter}, nil
|
||||
}
|
||||
|
||||
// 手机号已存在:进入账号合并或快捷登录流程
|
||||
finalUserID = targetUser.Id
|
||||
// 保护校验:若将不同用户进行合并,确保源用户不存在代理记录(临时用户不应为代理)
|
||||
l.Infof("[BindMobile] FindOneByMobile 命中 | targetUserId: %s, encryptedMobile: %s | 分支: 手机号已存在,进入合并或快捷登录", targetUser.Id, encryptedMobile)
|
||||
finalUserID := targetUser.Id
|
||||
if currentUserID != finalUserID {
|
||||
l.Infof("[BindMobile] 当前用户与手机号用户不同 | currentUserID: %s, finalUserID: %s, 需合并或绑定", currentUserID, finalUserID)
|
||||
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, currentUserID)
|
||||
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败: %v", err)
|
||||
}
|
||||
if agent != nil {
|
||||
l.Infof("[BindMobile] 拒绝: 源用户已是代理 | currentUserID: %s, agentId: %s", currentUserID, agent.Id)
|
||||
return nil, errors.Wrapf(xerr.NewErrMsg("账号数据异常:源用户存在代理记录,请联系技术支持"), "")
|
||||
}
|
||||
}
|
||||
// 查找当前登录态使用的认证(例如uuid或微信openid)是否已存在
|
||||
existingAuth, err := l.svcCtx.UserAuthModel.FindOneByAuthTypeAuthKey(l.ctx, currentAuthType, currentAuthKey)
|
||||
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找认证信息失败: %v", err)
|
||||
}
|
||||
// 如果当前认证已属于目标手机号用户,直接发放token(无需合并)
|
||||
if existingAuth != nil && existingAuth.UserId == finalUserID {
|
||||
l.Infof("[BindMobile] 当前认证已属于目标用户,直接发 token | finalUserID: %s", finalUserID)
|
||||
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成Token失败: %v", err)
|
||||
@@ -125,6 +133,7 @@ func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.Bind
|
||||
now := time.Now().Unix()
|
||||
return &types.BindMobileResp{AccessToken: token, AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire, RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter}, nil
|
||||
}
|
||||
l.Infof("[BindMobile] 需合并/绑定认证 | existingAuth: %v, finalUserID: %s, authType: %s", existingAuth != nil, finalUserID, currentAuthType)
|
||||
|
||||
// 微信唯一性约束(按类型):
|
||||
// - H5 与 小程序各自只能绑定一个 openid(互不影响)
|
||||
@@ -147,11 +156,7 @@ func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.Bind
|
||||
}
|
||||
}
|
||||
|
||||
// 事务处理:
|
||||
// - 将当前登录态的认证(uuid / 微信openid 等)绑定到目标手机号用户(finalUserID)
|
||||
// - 将源用户(currentUserID)的业务数据(订单、报告)迁移到目标用户,避免数据分裂
|
||||
// - 对源用户执行软删除,清理无主临时账号,保持数据一致性
|
||||
// 注意:所有步骤必须在同一个事务中执行,任何一步失败均会回滚,确保原子性
|
||||
l.Infof("[BindMobile] 开始事务: 合并认证与数据 | sourceUserId: %s, targetUserId: %s", currentUserID, finalUserID)
|
||||
err = l.svcCtx.UserModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error {
|
||||
// 1) 认证绑定处理(UUID替换策略)
|
||||
if currentAuthType == model.UserAuthTypeUUID {
|
||||
@@ -216,27 +221,32 @@ func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.Bind
|
||||
return err
|
||||
}
|
||||
|
||||
// 3) 源用户软删除
|
||||
// 软删源用户(通常为临时用户),防止遗留无效账号;软删可保留历史痕迹,满足审计需求
|
||||
// 3) 源用户软删除(若存在)
|
||||
// 源用户可能不存在(孤儿 auth:user 已删或从未写入),此时跳过软删即可
|
||||
currentUser, err := l.svcCtx.UserModel.FindOne(ctx, currentUserID)
|
||||
if err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找当前用户失败: %v", err)
|
||||
}
|
||||
if err := l.svcCtx.UserModel.Delete(ctx, session, currentUser.Id); err != nil {
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
l.Infof("[BindMobile] 源用户在 user 表中不存在,跳过软删 | sourceUserId: %s", currentUserID)
|
||||
} else {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找当前用户失败: %v", err)
|
||||
}
|
||||
} else if err := l.svcCtx.UserModel.Delete(ctx, session, currentUser.Id); err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "删除当前用户失败: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
l.Errorf("[BindMobile] 事务失败 | sourceUserId: %s, targetUserId: %s, err: %v", currentUserID, finalUserID, err)
|
||||
return nil, err
|
||||
}
|
||||
l.Infof("[BindMobile] 事务完成-合并成功 | finalUserID: %s", finalUserID)
|
||||
|
||||
// 合并完成后生成token(userType会根据mobile字段动态计算)
|
||||
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成Token失败: %v", err)
|
||||
}
|
||||
now := time.Now().Unix()
|
||||
l.Infof("[BindMobile] 完成-合并后发 token | finalUserID: %s, mobile: %s", finalUserID, req.Mobile)
|
||||
return &types.BindMobileResp{AccessToken: token, AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire, RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter}, nil
|
||||
}
|
||||
|
||||
@@ -26,13 +26,19 @@ func NewImageService() *ImageService {
|
||||
|
||||
// ProcessImageWithQRCode 处理图片,在中间添加二维码
|
||||
func (s *ImageService) ProcessImageWithQRCode(qrcodeType, qrcodeUrl string) ([]byte, string, error) {
|
||||
// 1. 根据qrcodeType确定使用哪张背景图(与 uniapp_ycc/src/static/invitation 下海报一致,需复制到本目录 static/images)
|
||||
// 1. 根据qrcodeType确定使用哪张背景图(与 uniapp static/invitation 下海报一致,需复制到本目录 static/images)
|
||||
var backgroundImageName string
|
||||
switch qrcodeType {
|
||||
case "promote":
|
||||
backgroundImageName = "tg_qrcode_1.png"
|
||||
case "invitation":
|
||||
backgroundImageName = "invitation_01.png" // 与 uniapp static/invitation/invitation_01.png 同一张图,部署时拷贝到 static/images
|
||||
case "invitation", "invitation_01":
|
||||
backgroundImageName = "invitation_01.png"
|
||||
case "invitation_02":
|
||||
backgroundImageName = "invitation_02.png"
|
||||
case "invitation_03":
|
||||
backgroundImageName = "invitation_03.png"
|
||||
case "invitation_04":
|
||||
backgroundImageName = "invitation_04.png"
|
||||
default:
|
||||
backgroundImageName = "tg_qrcode_1.png" // 默认使用第一张图片
|
||||
}
|
||||
@@ -77,12 +83,16 @@ func (s *ImageService) ProcessImageWithQRCode(qrcodeType, qrcodeUrl string) ([]b
|
||||
qrX = 192 // 距左边180px
|
||||
qrY = imgHeight - qrSize - 190 // 距底边100px
|
||||
|
||||
case "invitation":
|
||||
// invitation类型:精确设置二维码尺寸
|
||||
qrSize = 138 // 固定尺寸320px
|
||||
// 中间偏上位置
|
||||
qrX = (imgWidth - qrSize) / 2 // 水平居中
|
||||
qrY = 140 // 垂直位置200px
|
||||
case "invitation", "invitation_01":
|
||||
// invitation_01:二维码尺寸与位置
|
||||
qrSize = 138
|
||||
qrX = (imgWidth - qrSize) / 2
|
||||
qrY = 140
|
||||
case "invitation_02", "invitation_03", "invitation_04":
|
||||
// invitation_02/03/04:二维码往图片下面一点
|
||||
qrSize = 138
|
||||
qrX = (imgWidth - qrSize) / 2
|
||||
qrY = 160
|
||||
|
||||
default:
|
||||
// 默认(promote样式)
|
||||
@@ -152,7 +162,7 @@ func (s *ImageService) loadImage(path string) (image.Image, error) {
|
||||
|
||||
// GetSupportedImageTypes 获取支持的图片类型列表
|
||||
func (s *ImageService) GetSupportedImageTypes() []string {
|
||||
return []string{"promote", "invitation"}
|
||||
return []string{"promote", "invitation", "invitation_01", "invitation_02", "invitation_03", "invitation_04"}
|
||||
}
|
||||
|
||||
// CheckImageExists 检查指定类型的背景图是否存在
|
||||
@@ -161,8 +171,14 @@ func (s *ImageService) CheckImageExists(qrcodeType string) bool {
|
||||
switch qrcodeType {
|
||||
case "promote":
|
||||
backgroundImageName = "tg_qrcode_1.png"
|
||||
case "invitation":
|
||||
case "invitation", "invitation_01":
|
||||
backgroundImageName = "invitation_01.png"
|
||||
case "invitation_02":
|
||||
backgroundImageName = "invitation_02.png"
|
||||
case "invitation_03":
|
||||
backgroundImageName = "invitation_03.png"
|
||||
case "invitation_04":
|
||||
backgroundImageName = "invitation_04.png"
|
||||
default:
|
||||
backgroundImageName = "tg_qrcode_1.png"
|
||||
}
|
||||
|
||||
@@ -90,8 +90,10 @@ type AdminCreateApiResp struct {
|
||||
}
|
||||
|
||||
type AdminCreateFeatureReq struct {
|
||||
ApiId string `json:"api_id"` // API标识
|
||||
Name string `json:"name"` // 描述
|
||||
ApiId string `json:"api_id"` // API标识
|
||||
Name string `json:"name"` // 描述
|
||||
WhitelistPrice *float64 `json:"whitelist_price,optional"` // 白名单屏蔽价格(单位:元)
|
||||
CostPrice *float64 `json:"cost_price,optional"` // 天远API调用成本价(单位:元)
|
||||
}
|
||||
|
||||
type AdminCreateFeatureResp struct {
|
||||
@@ -401,11 +403,13 @@ type AdminGetFeatureDetailReq struct {
|
||||
}
|
||||
|
||||
type AdminGetFeatureDetailResp struct {
|
||||
Id string `json:"id"` // 功能ID
|
||||
ApiId string `json:"api_id"` // API标识
|
||||
Name string `json:"name"` // 描述
|
||||
CreateTime string `json:"create_time"` // 创建时间
|
||||
UpdateTime string `json:"update_time"` // 更新时间
|
||||
Id string `json:"id"` // 功能ID
|
||||
ApiId string `json:"api_id"` // API标识
|
||||
Name string `json:"name"` // 描述
|
||||
WhitelistPrice float64 `json:"whitelist_price"` // 白名单屏蔽价格(单位:元)
|
||||
CostPrice float64 `json:"cost_price"` // 天远API调用成本价(单位:元)
|
||||
CreateTime string `json:"create_time"` // 创建时间
|
||||
UpdateTime string `json:"update_time"` // 更新时间
|
||||
}
|
||||
|
||||
type AdminGetFeatureExampleReq struct {
|
||||
@@ -842,9 +846,11 @@ type AdminUpdateApiResp struct {
|
||||
}
|
||||
|
||||
type AdminUpdateFeatureReq struct {
|
||||
Id string `path:"id"` // 功能ID
|
||||
ApiId *string `json:"api_id,optional"` // API标识
|
||||
Name *string `json:"name,optional"` // 描述
|
||||
Id string `path:"id"` // 功能ID
|
||||
ApiId *string `json:"api_id,optional"` // API标识
|
||||
Name *string `json:"name,optional"` // 描述
|
||||
WhitelistPrice *float64 `json:"whitelist_price,optional"` // 白名单屏蔽价格(单位:元)
|
||||
CostPrice *float64 `json:"cost_price,optional"` // 天远API调用成本价(单位:元)
|
||||
}
|
||||
|
||||
type AdminUpdateFeatureResp struct {
|
||||
@@ -1317,11 +1323,13 @@ type Feature struct {
|
||||
}
|
||||
|
||||
type FeatureListItem struct {
|
||||
Id string `json:"id"` // 功能ID
|
||||
ApiId string `json:"api_id"` // API标识
|
||||
Name string `json:"name"` // 描述
|
||||
CreateTime string `json:"create_time"` // 创建时间
|
||||
UpdateTime string `json:"update_time"` // 更新时间
|
||||
Id string `json:"id"` // 功能ID
|
||||
ApiId string `json:"api_id"` // API标识
|
||||
Name string `json:"name"` // 描述
|
||||
WhitelistPrice float64 `json:"whitelist_price"` // 白名单屏蔽价格(单位:元)
|
||||
CostPrice float64 `json:"cost_price"` // 天远API调用成本价(单位:元)
|
||||
CreateTime string `json:"create_time"` // 创建时间
|
||||
UpdateTime string `json:"update_time"` // 更新时间
|
||||
}
|
||||
|
||||
type GenerateInviteCodeReq struct {
|
||||
@@ -1390,8 +1398,9 @@ type GetInviteLinkResp struct {
|
||||
}
|
||||
|
||||
type GetInvitePosterReq struct {
|
||||
InviteLink string `form:"invite_link"` // 邀请链接(短链)
|
||||
Format string `form:"format,optional"` // 返回格式:base64(默认)
|
||||
InviteLink string `form:"invite_link"` // 邀请链接(短链)
|
||||
Format string `form:"format,optional"` // 返回格式:base64(默认)
|
||||
TemplateIndex int `form:"template_index,optional"` // 模板序号 1-4,默认 1
|
||||
}
|
||||
|
||||
type GetInvitePosterResp struct {
|
||||
@@ -1887,11 +1896,13 @@ type ProductResponse struct {
|
||||
}
|
||||
|
||||
type PromotionQueryItem struct {
|
||||
Id string `json:"id"` // 查询ID
|
||||
OrderId string `json:"order_id"` // 订单ID
|
||||
ProductName string `json:"product_name"` // 产品名称
|
||||
CreateTime string `json:"create_time"` // 创建时间
|
||||
QueryState string `json:"query_state"` // 查询状态
|
||||
Id string `json:"id"` // 查询ID
|
||||
OrderId string `json:"order_id"` // 订单ID
|
||||
ProductName string `json:"product_name"` // 产品名称
|
||||
CreateTime string `json:"create_time"` // 创建时间
|
||||
QueryState string `json:"query_state"` // 查询状态
|
||||
Params map[string]interface{} `json:"params"` // 查询参数(已脱敏)
|
||||
Price float64 `json:"price"` // 查询价格
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
|
||||
BIN
app/main/api/static/images/invitation_02.png
Normal file
BIN
app/main/api/static/images/invitation_02.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 356 KiB |
BIN
app/main/api/static/images/invitation_03.png
Normal file
BIN
app/main/api/static/images/invitation_03.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 356 KiB |
BIN
app/main/api/static/images/invitation_04.png
Normal file
BIN
app/main/api/static/images/invitation_04.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 321 KiB |
Reference in New Issue
Block a user