f
This commit is contained in:
@@ -144,3 +144,52 @@ type ProductAdminListResponse struct {
|
|||||||
Size int `json:"size" comment:"每页数量"`
|
Size int `json:"size" comment:"每页数量"`
|
||||||
Items []ProductAdminInfoResponse `json:"items" comment:"产品列表"`
|
Items []ProductAdminInfoResponse `json:"items" comment:"产品列表"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PackageItemAIResponse AI组合包项目响应(不含成本价)
|
||||||
|
type PackageItemAIResponse struct {
|
||||||
|
ID string `json:"id" comment:"项目ID"`
|
||||||
|
ProductID string `json:"product_id" comment:"子产品ID"`
|
||||||
|
ProductCode string `json:"product_code" comment:"子产品编号"`
|
||||||
|
ProductName string `json:"product_name" comment:"子产品名称"`
|
||||||
|
SortOrder int `json:"sort_order" comment:"排序"`
|
||||||
|
Price float64 `json:"price" comment:"子产品价格"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProductAIInfoResponse AI产品详情响应(含启用/可见状态,不含用户状态、成本价、备注)
|
||||||
|
type ProductAIInfoResponse struct {
|
||||||
|
ID string `json:"id" comment:"产品ID"`
|
||||||
|
OldID *string `json:"old_id,omitempty" comment:"旧产品ID"`
|
||||||
|
Name string `json:"name" comment:"产品名称"`
|
||||||
|
Code string `json:"code" comment:"产品编号"`
|
||||||
|
Description string `json:"description" comment:"产品简介"`
|
||||||
|
Content string `json:"content" comment:"产品内容"`
|
||||||
|
CategoryID string `json:"category_id" comment:"一级分类ID"`
|
||||||
|
SubCategoryID *string `json:"sub_category_id,omitempty" comment:"二级分类ID"`
|
||||||
|
Price float64 `json:"price" comment:"产品价格"`
|
||||||
|
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||||
|
IsVisible bool `json:"is_visible" comment:"是否可见"`
|
||||||
|
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||||
|
|
||||||
|
SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"`
|
||||||
|
UIComponentPrice float64 `json:"ui_component_price" comment:"UI组件销售价格(组合包使用)"`
|
||||||
|
|
||||||
|
SEOTitle string `json:"seo_title" comment:"SEO标题"`
|
||||||
|
SEODescription string `json:"seo_description" comment:"SEO描述"`
|
||||||
|
SEOKeywords string `json:"seo_keywords" comment:"SEO关键词"`
|
||||||
|
|
||||||
|
Category *CategoryInfoResponse `json:"category,omitempty" comment:"一级分类信息"`
|
||||||
|
SubCategory *SubCategoryInfoResponse `json:"sub_category,omitempty" comment:"二级分类信息"`
|
||||||
|
|
||||||
|
PackageItems []*PackageItemAIResponse `json:"package_items,omitempty" comment:"组合包项目列表"`
|
||||||
|
|
||||||
|
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProductAIListResponse AI产品列表响应
|
||||||
|
type ProductAIListResponse struct {
|
||||||
|
Total int64 `json:"total" comment:"总数"`
|
||||||
|
Page int `json:"page" comment:"页码"`
|
||||||
|
Size int `json:"size" comment:"每页数量"`
|
||||||
|
Items []ProductAIInfoResponse `json:"items" comment:"产品列表"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ type ProductApplicationService interface {
|
|||||||
ListProductsForAdmin(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductAdminListResponse, error)
|
ListProductsForAdmin(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductAdminListResponse, error)
|
||||||
GetProductByIDForAdmin(ctx context.Context, query *queries.GetProductDetailQuery) (*responses.ProductAdminInfoResponse, error)
|
GetProductByIDForAdmin(ctx context.Context, query *queries.GetProductDetailQuery) (*responses.ProductAdminInfoResponse, error)
|
||||||
|
|
||||||
|
// AI专用方法
|
||||||
|
ListProductsForAI(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductAIListResponse, error)
|
||||||
|
|
||||||
// 用户端专用方法
|
// 用户端专用方法
|
||||||
GetProductByIDForUser(ctx context.Context, query *queries.GetProductDetailQuery) (*responses.ProductInfoWithDocumentResponse, error)
|
GetProductByIDForUser(ctx context.Context, query *queries.GetProductDetailQuery) (*responses.ProductInfoWithDocumentResponse, error)
|
||||||
|
|
||||||
|
|||||||
@@ -415,6 +415,27 @@ func (s *ProductApplicationServiceImpl) GetAvailableProducts(ctx context.Context
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListProductsForAI 获取产品列表(AI专用)
|
||||||
|
// 业务流程:1. 获取全部产品(含禁用/隐藏) 2. 构建AI响应数据(无用户状态、成本价、备注)
|
||||||
|
func (s *ProductApplicationServiceImpl) ListProductsForAI(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductAIListResponse, error) {
|
||||||
|
products, total, err := s.productManagementService.ListProducts(ctx, filters, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]responses.ProductAIInfoResponse, len(products))
|
||||||
|
for i := range products {
|
||||||
|
items[i] = *s.convertToProductAIInfoResponse(products[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return &responses.ProductAIListResponse{
|
||||||
|
Total: total,
|
||||||
|
Page: options.Page,
|
||||||
|
Size: options.PageSize,
|
||||||
|
Items: items,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ListProductsForAdmin 获取产品列表(管理员专用)
|
// ListProductsForAdmin 获取产品列表(管理员专用)
|
||||||
// 业务流程:1. 获取所有产品列表(包括隐藏的) 2. 构建管理员响应数据
|
// 业务流程:1. 获取所有产品列表(包括隐藏的) 2. 构建管理员响应数据
|
||||||
func (s *ProductApplicationServiceImpl) ListProductsForAdmin(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductAdminListResponse, error) {
|
func (s *ProductApplicationServiceImpl) ListProductsForAdmin(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductAdminListResponse, error) {
|
||||||
@@ -546,6 +567,55 @@ func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *en
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// convertToProductAIInfoResponse 转换为AI产品信息响应
|
||||||
|
func (s *ProductApplicationServiceImpl) convertToProductAIInfoResponse(product *entities.Product) *responses.ProductAIInfoResponse {
|
||||||
|
response := &responses.ProductAIInfoResponse{
|
||||||
|
ID: product.ID,
|
||||||
|
OldID: product.OldID,
|
||||||
|
Name: product.Name,
|
||||||
|
Code: product.Code,
|
||||||
|
Description: product.Description,
|
||||||
|
Content: product.Content,
|
||||||
|
CategoryID: product.CategoryID,
|
||||||
|
SubCategoryID: product.SubCategoryID,
|
||||||
|
Price: product.Price.InexactFloat64(),
|
||||||
|
IsEnabled: product.IsEnabled,
|
||||||
|
IsVisible: product.IsVisible,
|
||||||
|
IsPackage: product.IsPackage,
|
||||||
|
SellUIComponent: product.SellUIComponent,
|
||||||
|
UIComponentPrice: product.UIComponentPrice.InexactFloat64(),
|
||||||
|
SEOTitle: product.SEOTitle,
|
||||||
|
SEODescription: product.SEODescription,
|
||||||
|
SEOKeywords: product.SEOKeywords,
|
||||||
|
CreatedAt: product.CreatedAt,
|
||||||
|
UpdatedAt: product.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if product.Category != nil {
|
||||||
|
response.Category = s.convertToCategoryInfoResponse(product.Category)
|
||||||
|
}
|
||||||
|
|
||||||
|
if product.SubCategory != nil {
|
||||||
|
response.SubCategory = s.convertToSubCategoryInfoResponse(product.SubCategory)
|
||||||
|
}
|
||||||
|
|
||||||
|
if product.IsPackage && len(product.PackageItems) > 0 {
|
||||||
|
response.PackageItems = make([]*responses.PackageItemAIResponse, len(product.PackageItems))
|
||||||
|
for i, item := range product.PackageItems {
|
||||||
|
response.PackageItems[i] = &responses.PackageItemAIResponse{
|
||||||
|
ID: item.ID,
|
||||||
|
ProductID: item.ProductID,
|
||||||
|
ProductCode: item.Product.Code,
|
||||||
|
ProductName: item.Product.Name,
|
||||||
|
SortOrder: item.SortOrder,
|
||||||
|
Price: item.Product.Price.InexactFloat64(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
// convertToProductAdminInfoResponse 转换为管理员产品信息响应
|
// convertToProductAdminInfoResponse 转换为管理员产品信息响应
|
||||||
func (s *ProductApplicationServiceImpl) convertToProductAdminInfoResponse(product *entities.Product) *responses.ProductAdminInfoResponse {
|
func (s *ProductApplicationServiceImpl) convertToProductAdminInfoResponse(product *entities.Product) *responses.ProductAdminInfoResponse {
|
||||||
response := &responses.ProductAdminInfoResponse{
|
response := &responses.ProductAdminInfoResponse{
|
||||||
|
|||||||
@@ -1430,6 +1430,8 @@ func NewContainer() *Container {
|
|||||||
routes.NewPDFGRoutes,
|
routes.NewPDFGRoutes,
|
||||||
// 企业报告页面路由
|
// 企业报告页面路由
|
||||||
routes.NewQYGLReportRoutes,
|
routes.NewQYGLReportRoutes,
|
||||||
|
// AI产品路由
|
||||||
|
routes.NewAIProductRoutes,
|
||||||
),
|
),
|
||||||
|
|
||||||
// 应用生命周期
|
// 应用生命周期
|
||||||
@@ -1550,6 +1552,7 @@ func RegisterRoutes(
|
|||||||
publicQueryWhitelistRoutes *routes.PublicQueryWhitelistRoutes,
|
publicQueryWhitelistRoutes *routes.PublicQueryWhitelistRoutes,
|
||||||
pdfgRoutes *routes.PDFGRoutes,
|
pdfgRoutes *routes.PDFGRoutes,
|
||||||
qyglReportRoutes *routes.QYGLReportRoutes,
|
qyglReportRoutes *routes.QYGLReportRoutes,
|
||||||
|
aiProductRoutes *routes.AIProductRoutes,
|
||||||
jwtAuth *middleware.JWTAuthMiddleware,
|
jwtAuth *middleware.JWTAuthMiddleware,
|
||||||
adminAuth *middleware.AdminAuthMiddleware,
|
adminAuth *middleware.AdminAuthMiddleware,
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
@@ -1580,6 +1583,7 @@ func RegisterRoutes(
|
|||||||
publicQueryWhitelistRoutes.Register(router)
|
publicQueryWhitelistRoutes.Register(router)
|
||||||
pdfgRoutes.Register(router)
|
pdfgRoutes.Register(router)
|
||||||
qyglReportRoutes.Register(router)
|
qyglReportRoutes.Register(router)
|
||||||
|
aiProductRoutes.Register(router)
|
||||||
|
|
||||||
// 打印注册的路由信息
|
// 打印注册的路由信息
|
||||||
router.PrintRoutes()
|
router.PrintRoutes()
|
||||||
|
|||||||
@@ -166,6 +166,80 @@ func (h *ProductHandler) ListProducts(c *gin.Context) {
|
|||||||
h.responseBuilder.Success(c, result, "获取产品列表成功")
|
h.responseBuilder.Success(c, result, "获取产品列表成功")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListProductsForAI 获取产品列表(AI专用)
|
||||||
|
// @Summary 获取产品列表(AI专用)
|
||||||
|
// @Description AI调用专用产品列表,返回全部产品(含禁用/隐藏),包含 is_enabled/is_visible,不含用户订阅状态、成本价和备注
|
||||||
|
// @Tags AI接口
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param page query int false "页码" default(1)
|
||||||
|
// @Param page_size query int false "每页数量" default(10)
|
||||||
|
// @Param keyword query string false "搜索关键词"
|
||||||
|
// @Param category_id query string false "分类ID"
|
||||||
|
// @Param is_enabled query bool false "是否启用"
|
||||||
|
// @Param is_visible query bool false "是否可见"
|
||||||
|
// @Param is_package query bool false "是否组合包"
|
||||||
|
// @Param sort_by query string false "排序字段"
|
||||||
|
// @Param sort_order query string false "排序方向" Enums(asc, desc)
|
||||||
|
// @Success 200 {object} responses.ProductAIListResponse "获取产品列表成功"
|
||||||
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||||
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||||
|
// @Router /api/v1/ai/products [get]
|
||||||
|
func (h *ProductHandler) ListProductsForAI(c *gin.Context) {
|
||||||
|
page := h.getIntQuery(c, "page", 1)
|
||||||
|
pageSize := h.getIntQuery(c, "page_size", 10)
|
||||||
|
|
||||||
|
filters := make(map[string]interface{})
|
||||||
|
|
||||||
|
if keyword := c.Query("keyword"); keyword != "" {
|
||||||
|
filters["keyword"] = keyword
|
||||||
|
}
|
||||||
|
if categoryID := c.Query("category_id"); categoryID != "" {
|
||||||
|
filters["category_id"] = categoryID
|
||||||
|
}
|
||||||
|
if isEnabled := c.Query("is_enabled"); isEnabled != "" {
|
||||||
|
if enabled, err := strconv.ParseBool(isEnabled); err == nil {
|
||||||
|
filters["is_enabled"] = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isVisible := c.Query("is_visible"); isVisible != "" {
|
||||||
|
if visible, err := strconv.ParseBool(isVisible); err == nil {
|
||||||
|
filters["is_visible"] = visible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isPackage := c.Query("is_package"); isPackage != "" {
|
||||||
|
if pkg, err := strconv.ParseBool(isPackage); err == nil {
|
||||||
|
filters["is_package"] = pkg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sortBy := c.Query("sort_by")
|
||||||
|
if sortBy == "" {
|
||||||
|
sortBy = "created_at"
|
||||||
|
}
|
||||||
|
|
||||||
|
sortOrder := c.Query("sort_order")
|
||||||
|
if sortOrder == "" {
|
||||||
|
sortOrder = "desc"
|
||||||
|
}
|
||||||
|
|
||||||
|
options := interfaces.ListOptions{
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
Sort: sortBy,
|
||||||
|
Order: sortOrder,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.appService.ListProductsForAI(c.Request.Context(), filters, options)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("获取AI产品列表失败", zap.Error(err))
|
||||||
|
h.responseBuilder.InternalError(c, "获取产品列表失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.responseBuilder.Success(c, result, "获取产品列表成功")
|
||||||
|
}
|
||||||
|
|
||||||
// getIntQuery 获取整数查询参数
|
// getIntQuery 获取整数查询参数
|
||||||
func (h *ProductHandler) getIntQuery(c *gin.Context, key string, defaultValue int) int {
|
func (h *ProductHandler) getIntQuery(c *gin.Context, key string, defaultValue int) int {
|
||||||
if value := c.Query(key); value != "" {
|
if value := c.Query(key); value != "" {
|
||||||
|
|||||||
37
internal/infrastructure/http/routes/ai_product_routes.go
Normal file
37
internal/infrastructure/http/routes/ai_product_routes.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"tyapi-server/internal/infrastructure/http/handlers"
|
||||||
|
sharedhttp "tyapi-server/internal/shared/http"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AIProductRoutes AI产品接口路由
|
||||||
|
type AIProductRoutes struct {
|
||||||
|
productHandler *handlers.ProductHandler
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAIProductRoutes 创建AI产品路由
|
||||||
|
func NewAIProductRoutes(
|
||||||
|
productHandler *handlers.ProductHandler,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) *AIProductRoutes {
|
||||||
|
return &AIProductRoutes{
|
||||||
|
productHandler: productHandler,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register 注册AI产品相关路由
|
||||||
|
func (r *AIProductRoutes) Register(router *sharedhttp.GinRouter) {
|
||||||
|
engine := router.GetEngine()
|
||||||
|
|
||||||
|
aiProducts := engine.Group("/api/v1/ai/products")
|
||||||
|
{
|
||||||
|
aiProducts.GET("", r.productHandler.ListProductsForAI)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Info("AI产品路由注册完成")
|
||||||
|
}
|
||||||
238
scripts/export_ivyz4y27_calls.py
Normal file
238
scripts/export_ivyz4y27_calls.py
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
导出「河南钟馗科技有限公司」学历信息高级版(IVYZ4Y27)成功调用记录为 CSV。
|
||||||
|
解密 request_params,仅输出脱敏后的姓名、身份证。
|
||||||
|
|
||||||
|
依赖: pip install pycryptodome psycopg2-binary
|
||||||
|
|
||||||
|
用法 A - 直连数据库(在可访问生产/测试库的环境执行):
|
||||||
|
python export_ivyz4y27_calls.py \\
|
||||||
|
--from-db \\
|
||||||
|
--db-host tyapi-postgres-prod --db-name tyapi --db-user postgres --db-password <pwd> \\
|
||||||
|
-o ivyz4y27_zhongkui_calls.csv
|
||||||
|
|
||||||
|
或使用环境变量:
|
||||||
|
set PGHOST=... PGDATABASE=... PGUSER=... PGPASSWORD=...
|
||||||
|
python export_ivyz4y27_calls.py --from-db -o ivyz4y27_zhongkui_calls.csv
|
||||||
|
|
||||||
|
用法 B - 先 SQL 导出 JSON,再本地转 CSV:
|
||||||
|
python export_ivyz4y27_calls.py -i ivyz4y27_zhongkui_raw.json -o ivyz4y27_zhongkui_calls.csv
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
except ImportError:
|
||||||
|
print("请先安装: pip install pycryptodome", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
COMPANY_NAME = "河南钟馗科技有限公司"
|
||||||
|
PRODUCT_CODE = "IVYZ4Y27"
|
||||||
|
|
||||||
|
SQL = """
|
||||||
|
SELECT
|
||||||
|
ac.id,
|
||||||
|
ac.transaction_id,
|
||||||
|
ac.user_id,
|
||||||
|
ei.company_name,
|
||||||
|
p.code AS product_code,
|
||||||
|
p.name AS product_name,
|
||||||
|
ac.request_params,
|
||||||
|
au.secret_key,
|
||||||
|
ac.status,
|
||||||
|
ac.cost,
|
||||||
|
ac.start_at,
|
||||||
|
ac.created_at
|
||||||
|
FROM api_calls ac
|
||||||
|
INNER JOIN products p ON ac.product_id = p.id
|
||||||
|
INNER JOIN api_users au ON ac.user_id = au.user_id
|
||||||
|
INNER JOIN enterprise_infos ei ON ac.user_id = ei.user_id
|
||||||
|
WHERE ei.company_name = %s
|
||||||
|
AND ei.deleted_at IS NULL
|
||||||
|
AND p.code = %s
|
||||||
|
AND ac.status = 'success'
|
||||||
|
ORDER BY ac.created_at DESC
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def aes_decrypt(cipher_b64: str, key_hex: str) -> dict[str, Any]:
|
||||||
|
key = bytes.fromhex(key_hex.strip())
|
||||||
|
raw = base64.b64decode(cipher_b64.strip())
|
||||||
|
iv, ciphertext = raw[:16], raw[16:]
|
||||||
|
plain = AES.new(key, AES.MODE_CBC, iv).decrypt(ciphertext)
|
||||||
|
pad = plain[-1]
|
||||||
|
plain = plain[:-pad]
|
||||||
|
return json.loads(plain.decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def mask_name(name: str) -> str:
|
||||||
|
"""与 dwbg8b4d maskName 一致:张*三 / 李*"""
|
||||||
|
name = (name or "").strip()
|
||||||
|
if not name:
|
||||||
|
return ""
|
||||||
|
chars = list(name)
|
||||||
|
if len(chars) <= 1:
|
||||||
|
return name
|
||||||
|
if len(chars) == 2:
|
||||||
|
return chars[0] + "*"
|
||||||
|
return chars[0] + "*" + chars[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def mask_id_card(id_card: str) -> str:
|
||||||
|
"""与 query_whitelist_helpers MaskIDCard 一致:350681********0611"""
|
||||||
|
id_card = (id_card or "").strip().upper()
|
||||||
|
if len(id_card) <= 10:
|
||||||
|
return id_card
|
||||||
|
return id_card[:6] + "********" + id_card[-4:]
|
||||||
|
|
||||||
|
|
||||||
|
def extract_name_id_card(params: dict[str, Any]) -> tuple[str, str]:
|
||||||
|
name = params.get("name") or params.get("Name") or ""
|
||||||
|
id_card = params.get("id_card") or params.get("idCard") or params.get("IDCard") or ""
|
||||||
|
if isinstance(name, str):
|
||||||
|
name = name.strip()
|
||||||
|
else:
|
||||||
|
name = str(name)
|
||||||
|
if isinstance(id_card, str):
|
||||||
|
id_card = id_card.strip().upper()
|
||||||
|
else:
|
||||||
|
id_card = str(id_card).strip().upper()
|
||||||
|
return name, id_card
|
||||||
|
|
||||||
|
|
||||||
|
def load_rows_from_json(path: Path) -> list[dict]:
|
||||||
|
text = path.read_text(encoding="utf-8-sig").strip()
|
||||||
|
data = json.loads(text)
|
||||||
|
if isinstance(data, list):
|
||||||
|
return data
|
||||||
|
if isinstance(data, dict):
|
||||||
|
for key in ("records", "rows", "data", "items"):
|
||||||
|
if key in data and isinstance(data[key], list):
|
||||||
|
return data[key]
|
||||||
|
raise ValueError("JSON 应为数组,或 export SQL 的 json_agg 结果")
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_rows_from_db(args: argparse.Namespace) -> list[dict]:
|
||||||
|
try:
|
||||||
|
import psycopg2
|
||||||
|
import psycopg2.extras
|
||||||
|
except ImportError:
|
||||||
|
print("直连数据库需要: pip install psycopg2-binary", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
host = args.db_host or os.getenv("PGHOST", "localhost")
|
||||||
|
port = args.db_port or os.getenv("PGPORT", "5432")
|
||||||
|
dbname = args.db_name or os.getenv("PGDATABASE", "tyapi_dev")
|
||||||
|
user = args.db_user or os.getenv("PGUSER", "postgres")
|
||||||
|
password = args.db_password or os.getenv("PGPASSWORD", "")
|
||||||
|
|
||||||
|
conn = psycopg2.connect(
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
dbname=dbname,
|
||||||
|
user=user,
|
||||||
|
password=password,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
|
||||||
|
cur.execute(SQL, (COMPANY_NAME, PRODUCT_CODE))
|
||||||
|
rows = cur.fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def row_to_csv_record(row: dict) -> dict[str, str]:
|
||||||
|
encrypted = row.get("request_params") or ""
|
||||||
|
secret_key = row.get("secret_key") or ""
|
||||||
|
name_masked = ""
|
||||||
|
id_card_masked = ""
|
||||||
|
decrypt_error = ""
|
||||||
|
|
||||||
|
if encrypted and secret_key:
|
||||||
|
try:
|
||||||
|
params = aes_decrypt(encrypted, secret_key)
|
||||||
|
name, id_card = extract_name_id_card(params)
|
||||||
|
name_masked = mask_name(name)
|
||||||
|
id_card_masked = mask_id_card(id_card)
|
||||||
|
except Exception as exc:
|
||||||
|
decrypt_error = str(exc)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"company_name": str(row.get("company_name") or COMPANY_NAME),
|
||||||
|
"product_code": str(row.get("product_code") or PRODUCT_CODE),
|
||||||
|
"product_name": str(row.get("product_name") or ""),
|
||||||
|
"api_call_id": str(row.get("api_call_id") or row.get("id") or ""),
|
||||||
|
"transaction_id": str(row.get("transaction_id") or ""),
|
||||||
|
"call_time": str(row.get("call_time") or row.get("created_at") or ""),
|
||||||
|
"start_at": str(row.get("start_at") or ""),
|
||||||
|
"cost": str(row.get("cost") or ""),
|
||||||
|
"name_masked": name_masked,
|
||||||
|
"id_card_masked": id_card_masked,
|
||||||
|
"decrypt_error": decrypt_error,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def write_csv(records: list[dict[str, str]], output: Path) -> None:
|
||||||
|
fields = [
|
||||||
|
"company_name",
|
||||||
|
"product_code",
|
||||||
|
"product_name",
|
||||||
|
"api_call_id",
|
||||||
|
"transaction_id",
|
||||||
|
"call_time",
|
||||||
|
"start_at",
|
||||||
|
"cost",
|
||||||
|
"name_masked",
|
||||||
|
"id_card_masked",
|
||||||
|
"decrypt_error",
|
||||||
|
]
|
||||||
|
with output.open("w", encoding="utf-8-sig", newline="") as f:
|
||||||
|
writer = csv.DictWriter(f, fieldnames=fields)
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(records)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="导出 IVYZ4Y27 学历高级版调用记录 CSV")
|
||||||
|
parser.add_argument("-i", "--input", help="SQL 导出的 JSON 文件")
|
||||||
|
parser.add_argument("-o", "--output", default="ivyz4y27_zhongkui_calls.csv")
|
||||||
|
parser.add_argument("--from-db", action="store_true", help="直连 PostgreSQL 查询")
|
||||||
|
parser.add_argument("--db-host")
|
||||||
|
parser.add_argument("--db-port", default="5432")
|
||||||
|
parser.add_argument("--db-name")
|
||||||
|
parser.add_argument("--db-user")
|
||||||
|
parser.add_argument("--db-password")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.from_db:
|
||||||
|
rows = fetch_rows_from_db(args)
|
||||||
|
elif args.input:
|
||||||
|
rows = load_rows_from_json(Path(args.input))
|
||||||
|
else:
|
||||||
|
print("请指定 --from-db 或 -i <json文件>", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
records = [row_to_csv_record(r) for r in rows]
|
||||||
|
out = Path(args.output)
|
||||||
|
write_csv(records, out)
|
||||||
|
|
||||||
|
failed = sum(1 for r in records if r.get("decrypt_error"))
|
||||||
|
print(f"企业: {COMPANY_NAME}")
|
||||||
|
print(f"产品: {PRODUCT_CODE} (学历信息高级版)")
|
||||||
|
print(f"成功记录: {len(records)} 条, 解密失败: {failed} 条")
|
||||||
|
print(f"输出: {out.resolve()}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
33
scripts/export_ivyz4y27_zhongkui.sql
Normal file
33
scripts/export_ivyz4y27_zhongkui.sql
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
-- 河南钟馗科技有限公司 · 学历信息高级版(IVYZ4Y27) · 调用成功记录
|
||||||
|
-- 在 DBeaver / Navicat 中执行 → 结果集右键「导出 CSV」即可。
|
||||||
|
--
|
||||||
|
-- 说明:
|
||||||
|
-- request_params 为 AES 加密密文,客户端无法直接看到姓名/身份证。
|
||||||
|
-- 导出后可用 scripts/export_ivyz4y27_calls.py -i <导出的json> 解密并脱敏。
|
||||||
|
--
|
||||||
|
-- 若 JOIN products 报错,把 products 改成 product 再试。
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
ac.id AS api_call_id,
|
||||||
|
ac.transaction_id,
|
||||||
|
ac.created_at AS call_time,
|
||||||
|
ac.start_at,
|
||||||
|
ac.end_at,
|
||||||
|
ac.status,
|
||||||
|
ac.cost,
|
||||||
|
ei.company_name,
|
||||||
|
p.code AS product_code,
|
||||||
|
p.name AS product_name,
|
||||||
|
ac.user_id,
|
||||||
|
ac.access_id,
|
||||||
|
ac.request_params,
|
||||||
|
au.secret_key
|
||||||
|
FROM api_calls ac
|
||||||
|
INNER JOIN products p ON ac.product_id = p.id
|
||||||
|
INNER JOIN api_users au ON ac.user_id = au.user_id
|
||||||
|
INNER JOIN enterprise_infos ei ON ac.user_id = ei.user_id
|
||||||
|
WHERE ei.company_name = '河南钟馗科技有限公司'
|
||||||
|
AND ei.deleted_at IS NULL
|
||||||
|
AND p.code = 'IVYZ4Y27'
|
||||||
|
AND ac.status = 'success'
|
||||||
|
ORDER BY ac.created_at DESC;
|
||||||
Reference in New Issue
Block a user