Compare commits
117 Commits
bf4c114ee2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| fcbd534b57 | |||
| d564f4eb1b | |||
| e89459f093 | |||
| 18c92584d9 | |||
| ba463ae38d | |||
| e246271a24 | |||
| a1024ed4b2 | |||
| d6b78a5d6d | |||
| 61c6cc4f35 | |||
| cdd1e00745 | |||
| 46ba4e048c | |||
| 3156539319 | |||
| dad8abad16 | |||
| 5f62261c11 | |||
| a0b2105339 | |||
| 83e71ae81b | |||
| 8675961207 | |||
| 4bd6f51728 | |||
| cd1db5276a | |||
| 2f653be375 | |||
| 9c3fb97b3f | |||
| b6053983d9 | |||
| c3b16c0ffe | |||
| 5f6cca5369 | |||
| a01226c7c0 | |||
| e67465a58d | |||
| 75316b10cb | |||
| ebcf3be923 | |||
| cff3fb8814 | |||
| e76fcd89bb | |||
| 10605afe1e | |||
| d3554e8b44 | |||
| 35a2eb03d8 | |||
| 2a6ec6e3ca | |||
| 44d8b3d28c | |||
| cad1d354f5 | |||
| 711dc83e47 | |||
| 65fdc9bf21 | |||
| e9fe7ac303 | |||
| 130f49fb9d | |||
| d66ef0b15f | |||
| a6a2d8d9c5 | |||
| e095553ba8 | |||
| a73097aed3 | |||
| 8bbd098f97 | |||
| 5f0224ad3b | |||
| 9438ccee5e | |||
| 8771261118 | |||
| 96c5870aa0 | |||
| 5e658f2527 | |||
| e03e6b983c | |||
| 6ab9bb21e7 | |||
| 7c4bcefc81 | |||
| 8eec9685db | |||
| 6a801acee1 | |||
| 6120020a7c | |||
| da0990e015 | |||
| 80faf3cac0 | |||
| df1e8f25ed | |||
| bfe2f065c5 | |||
| 2fcf55deee | |||
| 9a1cf0d1d1 | |||
| 895b38ab88 | |||
| a96c153286 | |||
| 040f6eef65 | |||
| 947a983c67 | |||
| df6a51ae62 | |||
| 06b5aa97ec | |||
| 3775101081 | |||
| 39db1e9c1d | |||
| 15c6257762 | |||
| f9a6204b40 | |||
| 4de32c4c39 | |||
| a6f309e472 | |||
| c27b15af18 | |||
| 249ea0b15a | |||
| c193211463 | |||
| 521bfeb4ef | |||
| e0d9fd2791 | |||
| 58ba7e9f70 | |||
| 3779a7d66d | |||
| 8eb6dfc962 | |||
| c8af22f981 | |||
| d837624c0a | |||
| faf4b7f6a7 | |||
| baa45a8a05 | |||
| ec1decfdd9 | |||
| ca45be642b | |||
| bba34f817e | |||
| 4e8f9317f5 | |||
| ce2d4087bb | |||
| 0ce793ac61 | |||
| 12ed1c81e3 | |||
| 6f0a8e0519 | |||
| 14b2c53eeb | |||
| 09db8d003e | |||
| 209ffec51d | |||
| f16274d1e9 | |||
| 869b269fb1 | |||
| 9e76fd467b | |||
| 1a5e771420 | |||
| 2114f602de | |||
| 5650e78254 | |||
| 67c6e2e144 | |||
| ba1a72aa8f | |||
| 058e355d77 | |||
| 2741839cf3 | |||
| 454e60dd72 | |||
| 03cb6fd92b | |||
| 8441e66e93 | |||
| 16a2e4ff09 | |||
| c5970da195 | |||
| 1bcb4a9c2e | |||
| 8877cf9691 | |||
| f63e6df9f9 | |||
| a00fe12141 | |||
| 6b80182986 |
24
Dockerfile
24
Dockerfile
@@ -37,12 +37,27 @@ FROM alpine:3.19
|
||||
# 设置Alpine镜像源
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
|
||||
# 安装必要的包
|
||||
RUN apk --no-cache add tzdata curl
|
||||
# 安装必要的包(包含 headless Chrome 所需依赖)
|
||||
# - tzdata: 时区
|
||||
# - curl: 健康检查
|
||||
# - chromium: 无头浏览器,用于 chromedp 生成 HTML 报告 PDF
|
||||
# - nss、freetype、harfbuzz、ttf-freefont、font-noto-cjk: 字体及渲染依赖,避免中文/图标丢失和乱码
|
||||
RUN apk --no-cache add \
|
||||
tzdata \
|
||||
curl \
|
||||
chromium \
|
||||
nss \
|
||||
freetype \
|
||||
harfbuzz \
|
||||
ttf-freefont \
|
||||
font-noto-cjk
|
||||
|
||||
# 设置时区
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
# 为 chromedp 指定默认的 Chrome 路径(Alpine 下 chromium 包的可执行文件)
|
||||
ENV CHROME_BIN=/usr/bin/chromium-browser
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
@@ -53,9 +68,8 @@ COPY --from=builder /app/tyapi-server .
|
||||
COPY config.yaml .
|
||||
COPY configs/ ./configs/
|
||||
|
||||
# 复制资源文件(直接从构建上下文复制,与配置文件一致)
|
||||
COPY resources/etc ./resources/etc
|
||||
COPY resources/pdf ./resources/pdf
|
||||
# 复制资源文件(报告模板、PDF、组件等)
|
||||
COPY resources ./resources
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080
|
||||
|
||||
77
cmd/qygl_report_build/main.go
Normal file
77
cmd/qygl_report_build/main.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// 将 raw fixture(各处理器原始 JSON)经 BuildReportFromRawSources 转化为与线上一致的完整报告 JSON。
|
||||
//
|
||||
// go run ./cmd/qygl_report_build -in resources/dev-report/fixture.raw.example.json -out resources/dev-report/built.json
|
||||
// go run ./cmd/qygl_report_build -in raw.json -out - # 输出到 stdout
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"tyapi-server/internal/domains/api/services/processors/qygl"
|
||||
)
|
||||
|
||||
type rawBundle struct {
|
||||
Kind string `json:"kind"`
|
||||
|
||||
JiguangFull map[string]interface{} `json:"jiguangFull"`
|
||||
JudicialCertFull map[string]interface{} `json:"judicialCertFull"`
|
||||
EquityPanorama map[string]interface{} `json:"equityPanorama"`
|
||||
AnnualReport map[string]interface{} `json:"annualReport"`
|
||||
TaxViolation map[string]interface{} `json:"taxViolation"`
|
||||
TaxArrears map[string]interface{} `json:"taxArrears"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
inPath := flag.String("in", "", "raw fixture JSON 路径(含 jiguangFull 等字段,可参考 fixture.raw.example.json)")
|
||||
outPath := flag.String("out", "", "输出文件;- 或留空表示输出到 stdout")
|
||||
flag.Parse()
|
||||
|
||||
if *inPath == "" {
|
||||
log.Fatal("请指定 -in <raw.json>")
|
||||
}
|
||||
raw, err := os.ReadFile(*inPath)
|
||||
if err != nil {
|
||||
log.Fatalf("读取输入失败: %v", err)
|
||||
}
|
||||
|
||||
var b rawBundle
|
||||
if err := json.Unmarshal(raw, &b); err != nil {
|
||||
log.Fatalf("解析 JSON 失败: %v", err)
|
||||
}
|
||||
|
||||
if b.Kind == "full" {
|
||||
log.Fatal("输入为 kind=full(已是 build 结果),无需再转化;预览请用: go run ./cmd/qygl_report_preview")
|
||||
}
|
||||
if b.Kind != "" && b.Kind != "raw" {
|
||||
log.Fatalf("若填写 kind,仅支持 raw,当前: %q", b.Kind)
|
||||
}
|
||||
|
||||
report := qygl.BuildReportFromRawSources(
|
||||
b.JiguangFull,
|
||||
b.JudicialCertFull,
|
||||
b.EquityPanorama,
|
||||
b.AnnualReport,
|
||||
b.TaxViolation,
|
||||
b.TaxArrears,
|
||||
)
|
||||
|
||||
out, err := json.MarshalIndent(report, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("序列化报告失败: %v", err)
|
||||
}
|
||||
|
||||
if *outPath == "" || *outPath == "-" {
|
||||
if _, err := os.Stdout.Write(append(out, '\n')); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(*outPath, append(out, '\n'), 0644); err != nil {
|
||||
log.Fatalf("写入失败: %v", err)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "已写入 %s\n", *outPath)
|
||||
}
|
||||
60
cmd/qygl_report_pdf/main.go
Normal file
60
cmd/qygl_report_pdf/main.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/shared/pdf"
|
||||
)
|
||||
|
||||
// 一个本地调试用的小工具:
|
||||
// 从 JSON 文件(企业报告.json)读取 QYGL 聚合结果,使用 gofpdf 生成企业全景报告 PDF,输出到当前目录。
|
||||
func main() {
|
||||
var (
|
||||
jsonPath string
|
||||
outPath string
|
||||
)
|
||||
flag.StringVar(&jsonPath, "json", "企业报告.json", "企业报告 JSON 数据源文件路径")
|
||||
flag.StringVar(&outPath, "out", "企业全景报告_gofpdf.pdf", "输出 PDF 文件路径")
|
||||
flag.Parse()
|
||||
|
||||
logger, _ := zap.NewDevelopment()
|
||||
defer logger.Sync()
|
||||
|
||||
absJSON, _ := filepath.Abs(jsonPath)
|
||||
fmt.Printf("读取 JSON 数据源:%s\n", absJSON)
|
||||
|
||||
data, err := os.ReadFile(jsonPath)
|
||||
if err != nil {
|
||||
fmt.Printf("读取 JSON 文件失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var report map[string]interface{}
|
||||
if err := json.Unmarshal(data, &report); err != nil {
|
||||
fmt.Printf("解析 JSON 失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("开始使用 gofpdf 生成企业全景报告 PDF...")
|
||||
pdfBytes, err := pdf.GenerateQYGLReportPDF(context.Background(), logger, report)
|
||||
if err != nil {
|
||||
fmt.Printf("生成 PDF 失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(outPath, pdfBytes, 0644); err != nil {
|
||||
fmt.Printf("写入 PDF 文件失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
absOut, _ := filepath.Abs(outPath)
|
||||
fmt.Printf("PDF 生成完成:%s\n", absOut)
|
||||
}
|
||||
|
||||
159
cmd/qygl_report_preview/main.go
Normal file
159
cmd/qygl_report_preview/main.go
Normal file
@@ -0,0 +1,159 @@
|
||||
// 仅读取 build 后的报告 JSON,本地渲染 qiye.html(不执行 BuildReportFromRawSources)。
|
||||
//
|
||||
// go run ./cmd/qygl_report_preview -in resources/dev-report/built.json
|
||||
// go run ./cmd/qygl_report_preview -in built.json -addr :8899 -watch
|
||||
//
|
||||
// 每次打开/刷新页面都会重新读取 -in 文件;加 -watch 后保存 JSON 会自动刷新浏览器。
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func parseBuiltReport(data []byte) (map[string]interface{}, error) {
|
||||
var root map[string]interface{}
|
||||
if err := json.Unmarshal(data, &root); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := root["jiguangFull"]; ok {
|
||||
return nil, fmt.Errorf("检测到 raw 字段 jiguangFull,请先执行: go run ./cmd/qygl_report_build -in <raw.json> -out built.json")
|
||||
}
|
||||
if k, _ := root["kind"].(string); k == "full" {
|
||||
r, ok := root["report"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("kind=full 时缺少 report 对象")
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
if r, ok := root["report"].(map[string]interface{}); ok {
|
||||
return r, nil
|
||||
}
|
||||
if root["entName"] != nil || root["basic"] != nil || root["reportTime"] != nil {
|
||||
return root, nil
|
||||
}
|
||||
return nil, fmt.Errorf("不是有效的 build 后报告(根级应有 entName、basic、reportTime 之一,或 {\"report\":{...}} / kind=full)")
|
||||
}
|
||||
|
||||
func fileVersionTag(path string) (string, error) {
|
||||
st, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%d-%d", st.ModTime().UnixNano(), st.Size()), nil
|
||||
}
|
||||
|
||||
func renderPage(tmpl *template.Template, report map[string]interface{}, injectLive bool) ([]byte, error) {
|
||||
reportBytes, err := json.Marshal(report)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, map[string]interface{}{
|
||||
"ReportJSON": template.JS(reportBytes),
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b := buf.Bytes()
|
||||
if !injectLive {
|
||||
return b, nil
|
||||
}
|
||||
script := `<script>(function(){var v0=null;function tick(){fetch("/__version?="+Date.now(),{cache:"no-store"}).then(function(r){return r.text();}).then(function(v){if(v==="")return;if(v0===null)v0=v;else if(v0!==v){v0=v;location.reload();}}).catch(function(){});}setInterval(tick,600);tick();})();</script>`
|
||||
closing := []byte("</body>")
|
||||
idx := bytes.LastIndex(b, closing)
|
||||
if idx < 0 {
|
||||
return append(b, []byte(script)...), nil
|
||||
}
|
||||
out := make([]byte, 0, len(b)+len(script))
|
||||
out = append(out, b[:idx]...)
|
||||
out = append(out, script...)
|
||||
out = append(out, b[idx:]...)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
addr := flag.String("addr", ":8899", "监听地址")
|
||||
root := flag.String("root", ".", "项目根目录(含 resources/qiye.html)")
|
||||
inPath := flag.String("in", "", "build 后的 JSON(由 qygl_report_build 生成,或 fixture.full 中的 report 形态)")
|
||||
watch := flag.Bool("watch", false, "监听 -in 文件变化并自动刷新浏览器(轮询)")
|
||||
flag.Parse()
|
||||
|
||||
if *inPath == "" {
|
||||
log.Fatal("请指定 -in <built.json>")
|
||||
}
|
||||
|
||||
rootAbs, err := filepath.Abs(*root)
|
||||
if err != nil {
|
||||
log.Fatalf("解析 root: %v", err)
|
||||
}
|
||||
tplPath := filepath.Join(rootAbs, "resources", "qiye.html")
|
||||
if _, err := os.Stat(tplPath); err != nil {
|
||||
log.Fatalf("未找到模板 %s: %v", tplPath, err)
|
||||
}
|
||||
|
||||
var inAbs string
|
||||
if filepath.IsAbs(*inPath) {
|
||||
inAbs = *inPath
|
||||
} else {
|
||||
inAbs = filepath.Join(rootAbs, *inPath)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(inAbs); err != nil {
|
||||
log.Fatalf("读取 %s: %v", inAbs, err)
|
||||
}
|
||||
|
||||
tmpl, err := template.ParseFiles(tplPath)
|
||||
if err != nil {
|
||||
log.Fatalf("解析模板: %v", err)
|
||||
}
|
||||
|
||||
http.HandleFunc("/__version", func(w http.ResponseWriter, r *http.Request) {
|
||||
tag, err := fileVersionTag(inAbs)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
_, _ = w.Write([]byte(tag))
|
||||
})
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
raw, err := os.ReadFile(inAbs)
|
||||
if err != nil {
|
||||
http.Error(w, "读取报告文件失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
report, err := parseBuiltReport(raw)
|
||||
if err != nil {
|
||||
http.Error(w, "解析 JSON 失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
html, err := renderPage(tmpl, report, *watch)
|
||||
if err != nil {
|
||||
http.Error(w, "渲染失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = w.Write(html)
|
||||
})
|
||||
|
||||
log.Printf("报告预览: http://127.0.0.1%s/ (每请求重读 %s)", *addr, inAbs)
|
||||
if *watch {
|
||||
log.Printf("已启用 -watch:保存 JSON 后约 0.6s 内自动刷新页面")
|
||||
}
|
||||
if err := http.ListenAndServe(*addr, nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
44
config.yaml
44
config.yaml
@@ -5,6 +5,8 @@ app:
|
||||
name: "TYAPI Server"
|
||||
version: "1.0.0"
|
||||
env: "development"
|
||||
# 子账号入口与主站可同域;邀请链接 {sub_portal_base_url}/sub/auth/register?invite=...
|
||||
sub_portal_base_url: "http://localhost:5173/"
|
||||
|
||||
server:
|
||||
host: "0.0.0.0"
|
||||
@@ -119,6 +121,7 @@ jwt:
|
||||
|
||||
api:
|
||||
domain: "api.tianyuanapi.com"
|
||||
# public_base_url: "" # 可选,无尾斜杠;空则按 https://{domain} 推导;环境变量 API_PUBLIC_BASE_URL 优先
|
||||
|
||||
sms:
|
||||
access_key_id: "LTAI5tKGB3TVJbMHSoZN3yr9"
|
||||
@@ -130,13 +133,13 @@ sms:
|
||||
expire_time: 5m
|
||||
mock_enabled: false
|
||||
# 签名验证配置(用于防止接口被刷)
|
||||
signature_enabled: true # 是否启用签名验证
|
||||
signature_secret: "TyApi2024SMSSecretKey!@#$%^&*()_+QWERTYUIOP" # 签名密钥(请修改为复杂密钥)
|
||||
signature_enabled: true # 是否启用签名验证
|
||||
signature_secret: "TyApi2024SMSSecretKey!@#$%^&*()_+QWERTYUIOP" # 签名密钥(请修改为复杂密钥)
|
||||
# 滑块验证码配置
|
||||
captcha_enabled: true # 是否启用滑块验证码
|
||||
captcha_secret: "" # 阿里云验证码密钥(加密模式时需要,可选)EKEY
|
||||
captcha_endpoint: "captcha.cn-shanghai.aliyuncs.com" # 阿里云验证码服务Endpoint
|
||||
scene_id: "wynt39to" # 阿里云验证码场景ID
|
||||
captcha_enabled: true # 是否启用滑块验证码
|
||||
captcha_secret: "" # 阿里云验证码密钥(加密模式时需要,可选)EKEY
|
||||
captcha_endpoint: "captcha.cn-shanghai.aliyuncs.com" # 阿里云验证码服务Endpoint
|
||||
scene_id: "wynt39to" # 阿里云验证码场景ID
|
||||
rate_limit:
|
||||
daily_limit: 10
|
||||
hourly_limit: 5
|
||||
@@ -205,7 +208,7 @@ daily_ratelimit:
|
||||
enable_referer: true # 是否检查Referer
|
||||
allowed_referers: # 允许的Referer
|
||||
- "https://console.tianyuanapi.com" # 天元API控制台
|
||||
- "https://consoletest.tianyuanapi.com" # 天元API测试控制台
|
||||
- "https://subsole.tianyuanapi.com" # 天元API子账号控制台
|
||||
|
||||
enable_proxy_check: false # 是否检查代理
|
||||
enable_geo_block: false # 是否启用地理位置阻止
|
||||
@@ -236,7 +239,7 @@ development:
|
||||
debug: true
|
||||
enable_profiler: true
|
||||
enable_cors: true
|
||||
cors_allowed_origins: "http://localhost:5173,https://consoletest.tianyuanapi.com,https://console.tianyuanapi.com"
|
||||
cors_allowed_origins: "http://localhost:5173,https://console.tianyuanapi.com,https://subsole.tianyuanapi.com"
|
||||
cors_allowed_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS"
|
||||
cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With,Access-Id"
|
||||
|
||||
@@ -398,6 +401,7 @@ WechatH5:
|
||||
# ===========================================
|
||||
# 🔍 天眼查配置
|
||||
# ===========================================
|
||||
|
||||
tianyancha:
|
||||
base_url: http://open.api.tianyancha.com/services
|
||||
api_key: e6a43dc9-786e-4a16-bb12-392b8201d8e2
|
||||
@@ -547,20 +551,20 @@ jiguang:
|
||||
# ===========================================
|
||||
pdfgen:
|
||||
# 服务地址配置
|
||||
development_url: "http://pdfg.tianyuanapi.com" # 开发环境服务地址
|
||||
production_url: "http://1.117.67.95:15990" # 生产环境服务地址
|
||||
|
||||
development_url: "http://pdfg.tianyuanapi.com" # 开发环境服务地址
|
||||
production_url: "http://1.117.67.95:15990" # 生产环境服务地址
|
||||
|
||||
# API路径配置
|
||||
api_path: "/api/v1/generate/guangzhou" # PDF生成API路径
|
||||
|
||||
api_path: "/api/v1/generate/guangzhou" # PDF生成API路径
|
||||
|
||||
# 超时配置
|
||||
timeout: 120s # 请求超时时间(120秒)
|
||||
|
||||
timeout: 120s # 请求超时时间(120秒)
|
||||
|
||||
# 缓存配置
|
||||
cache:
|
||||
ttl: 24h # 缓存过期时间(24小时)
|
||||
cache_dir: "" # 缓存目录(空则使用默认目录)
|
||||
max_size: 0 # 最大缓存大小(0表示不限制,单位:字节)
|
||||
ttl: 24h # 缓存过期时间(24小时)
|
||||
cache_dir: "" # 缓存目录(空则使用默认目录)
|
||||
max_size: 0 # 最大缓存大小(0表示不限制,单位:字节)
|
||||
|
||||
# ===========================================
|
||||
# ✨ 数脉配置走实时接口
|
||||
@@ -605,7 +609,6 @@ shumai:
|
||||
max_age: 30
|
||||
compress: true
|
||||
|
||||
|
||||
# ===========================================
|
||||
# ✨ 数据宝配置走实时接口
|
||||
# ===========================================
|
||||
@@ -638,6 +641,3 @@ shujubao:
|
||||
max_backups: 5
|
||||
max_age: 30
|
||||
compress: true
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
# ===========================================
|
||||
app:
|
||||
env: development
|
||||
# 子账号专属前端域名(用于邀请链接复制)
|
||||
sub_portal_base_url: "http://localhost:5173"
|
||||
|
||||
# ===========================================
|
||||
# 🗄️ 数据库配置
|
||||
@@ -20,6 +22,10 @@ database:
|
||||
jwt:
|
||||
secret: JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW
|
||||
|
||||
# 本地联调:企业报告链接与 headless PDF 需能访问到本机服务;端口与 server 监听一致。环境变量 API_PUBLIC_BASE_URL 可覆盖。
|
||||
api:
|
||||
public_base_url: "http://127.0.0.1:8080"
|
||||
|
||||
# ===========================================
|
||||
# 📁 存储服务配置 - 七牛云
|
||||
# ===========================================
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
# ===========================================
|
||||
app:
|
||||
env: production
|
||||
# 子账号专属前端域名(用于邀请链接复制)
|
||||
sub_portal_base_url: "https://subsole.tianyuanapi.com"
|
||||
|
||||
# ===========================================
|
||||
# 🌐 服务器配置
|
||||
@@ -18,7 +20,7 @@ server:
|
||||
# ===========================================
|
||||
development:
|
||||
enable_cors: true
|
||||
cors_allowed_origins: "http://localhost:5173,https://consoletest.tianyuanapi.com,https://console.tianyuanapi.com"
|
||||
cors_allowed_origins: "https://console.tianyuanapi.com,https://subsole.tianyuanapi.com"
|
||||
cors_allowed_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS"
|
||||
cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With,Access-Id"
|
||||
|
||||
@@ -52,6 +54,8 @@ jwt:
|
||||
|
||||
api:
|
||||
domain: "api.tianyuanapi.com"
|
||||
# 可选:对外可访问的 API 完整基址(无尾斜杠),用于企业报告 reportUrl、PDF 预生成等;不设则按 https://{domain} 推导。环境变量 API_PUBLIC_BASE_URL 优先于本项。
|
||||
# public_base_url: "https://api.tianyuanapi.com"
|
||||
# ===========================================
|
||||
# 📁 存储服务配置 - 七牛云
|
||||
# ===========================================
|
||||
@@ -155,7 +159,7 @@ daily_ratelimit:
|
||||
enable_referer: true # 启用Referer检查
|
||||
allowed_referers: # 允许的Referer
|
||||
- "https://console.tianyuanapi.com"
|
||||
- "https://consoletest.tianyuanapi.com"
|
||||
- "https://subsole.tianyuanapi.com"
|
||||
|
||||
enable_geo_block: false # 生产环境暂时不启用地理位置阻止
|
||||
enable_proxy_check: true # 启用代理检查
|
||||
|
||||
@@ -89,7 +89,8 @@ services:
|
||||
- "25000:8080"
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
- ./resources/Pure_Component:/app/resources/Pure_Component
|
||||
# 挂载完整 resources 目录(包含 qiye.html、Pure_Component、pdf 等)
|
||||
- ./resources:/app/resources
|
||||
# 持久化PDF缓存目录,确保生成的PDF在容器重启后仍然存在
|
||||
- ./storage/pdfg-cache:/app/storage/pdfg-cache
|
||||
# user: "1001:1001" # 注释掉,使用root权限运行
|
||||
|
||||
10
go.mod
10
go.mod
@@ -58,6 +58,9 @@ require (
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 // indirect
|
||||
github.com/chromedp/chromedp v0.13.2 // indirect
|
||||
github.com/chromedp/sysutil v1.1.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
@@ -66,6 +69,7 @@ require (
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gammazero/toposort v0.1.1 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
@@ -73,6 +77,9 @@ require (
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/gofrs/flock v0.8.1 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
|
||||
@@ -87,12 +94,15 @@ require (
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260313013624-04e51e218220 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/oschwald/geoip2-golang v1.13.0 // indirect
|
||||
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
|
||||
20
go.sum
20
go.sum
@@ -80,6 +80,12 @@ github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F9
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 h1:AqW2bDQf67Zbq6Tpop/+yJSIknxhiQecO2B8jNYTAPs=
|
||||
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
|
||||
github.com/chromedp/chromedp v0.13.2 h1:f6sZFFzCzPLvWSzeuXQBgONKG7zPq54YfEyEj0EplOY=
|
||||
github.com/chromedp/chromedp v0.13.2/go.mod h1:khsDP9OP20GrowpJfZ7N05iGCwcAYxk7qf9AZBzR3Qw=
|
||||
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
|
||||
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
|
||||
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
@@ -115,6 +121,8 @@ github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fq
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 h1:yE7argOs92u+sSCRgqqe6eF+cDaVhSPlioy1UkA0p/w=
|
||||
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -146,6 +154,12 @@ github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc
|
||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
@@ -234,6 +248,8 @@ github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgx
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260313013624-04e51e218220 h1:FLQyP/6tTsTEtAhcIq/kS/zkDEMdOMon0I70pXVehOU=
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260313013624-04e51e218220/go.mod h1:+mNMTBuDMdEGhWzoQgc6kBdqeaQpWh5ba8zqmp2MxCU=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
@@ -252,6 +268,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
|
||||
github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI=
|
||||
github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
|
||||
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
|
||||
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
|
||||
@@ -26,9 +26,11 @@ import (
|
||||
articleEntities "tyapi-server/internal/domains/article/entities"
|
||||
|
||||
// 统计域实体
|
||||
securityEntities "tyapi-server/internal/domains/security/entities"
|
||||
statisticsEntities "tyapi-server/internal/domains/statistics/entities"
|
||||
|
||||
apiEntities "tyapi-server/internal/domains/api/entities"
|
||||
subordinateEntities "tyapi-server/internal/domains/subordinate/entities"
|
||||
"tyapi-server/internal/infrastructure/database"
|
||||
taskEntities "tyapi-server/internal/infrastructure/task/entities"
|
||||
)
|
||||
@@ -256,12 +258,21 @@ func (a *Application) autoMigrate(db *gorm.DB) error {
|
||||
&statisticsEntities.StatisticsMetric{},
|
||||
&statisticsEntities.StatisticsDashboard{},
|
||||
&statisticsEntities.StatisticsReport{},
|
||||
&securityEntities.SuspiciousIPRecord{},
|
||||
|
||||
// api
|
||||
&apiEntities.ApiUser{},
|
||||
&apiEntities.ApiCall{},
|
||||
&apiEntities.Report{},
|
||||
|
||||
// 下属账号域
|
||||
&subordinateEntities.SubordinateInvitation{},
|
||||
&subordinateEntities.UserSubordinateLink{},
|
||||
&subordinateEntities.SubordinateWalletAllocation{},
|
||||
&subordinateEntities.SubordinateQuotaPurchase{},
|
||||
&subordinateEntities.UserProductQuotaAccount{},
|
||||
&subordinateEntities.UserProductQuotaLedger{},
|
||||
|
||||
// 任务域
|
||||
&taskEntities.AsyncTask{},
|
||||
)
|
||||
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
finance_services "tyapi-server/internal/domains/finance/services"
|
||||
product_entities "tyapi-server/internal/domains/product/entities"
|
||||
product_services "tyapi-server/internal/domains/product/services"
|
||||
subordinate_entities "tyapi-server/internal/domains/subordinate/entities"
|
||||
subordinate_repositories "tyapi-server/internal/domains/subordinate/repositories"
|
||||
user_repositories "tyapi-server/internal/domains/user/repositories"
|
||||
task_entities "tyapi-server/internal/infrastructure/task/entities"
|
||||
"tyapi-server/internal/infrastructure/task/interfaces"
|
||||
@@ -93,6 +95,7 @@ type ApiApplicationServiceImpl struct {
|
||||
walletService finance_services.WalletAggregateService
|
||||
subscriptionService *product_services.ProductSubscriptionService
|
||||
balanceAlertService finance_services.BalanceAlertService
|
||||
subordinateRepo subordinate_repositories.SubordinateRepository
|
||||
}
|
||||
|
||||
func NewApiApplicationService(
|
||||
@@ -112,6 +115,7 @@ func NewApiApplicationService(
|
||||
subscriptionService *product_services.ProductSubscriptionService,
|
||||
exportManager *export.ExportManager,
|
||||
balanceAlertService finance_services.BalanceAlertService,
|
||||
subordinateRepo subordinate_repositories.SubordinateRepository,
|
||||
) ApiApplicationService {
|
||||
service := &ApiApplicationServiceImpl{
|
||||
apiCallService: apiCallService,
|
||||
@@ -130,6 +134,7 @@ func NewApiApplicationService(
|
||||
walletService: walletService,
|
||||
subscriptionService: subscriptionService,
|
||||
balanceAlertService: balanceAlertService,
|
||||
subordinateRepo: subordinateRepo,
|
||||
}
|
||||
|
||||
return service
|
||||
@@ -226,13 +231,19 @@ func (s *ApiApplicationServiceImpl) validateApiCall(ctx context.Context, cmd *co
|
||||
|
||||
// 4. 验证IP白名单(非开发环境)
|
||||
if !s.config.App.IsDevelopment() && !cmd.Options.IsDebug {
|
||||
whiteListIPs := make([]string, 0, len(apiUser.WhiteList))
|
||||
for _, item := range apiUser.WhiteList {
|
||||
whiteListIPs = append(whiteListIPs, item.IPAddress)
|
||||
}
|
||||
|
||||
// 添加调试日志
|
||||
s.logger.Info("开始验证白名单",
|
||||
zap.String("userId", apiUser.UserId),
|
||||
zap.String("clientIP", cmd.ClientIP),
|
||||
zap.Bool("isDevelopment", s.config.App.IsDevelopment()),
|
||||
zap.Bool("isDebug", cmd.Options.IsDebug),
|
||||
zap.Int("whiteListCount", len(apiUser.WhiteList)))
|
||||
zap.Int("whiteListCount", len(apiUser.WhiteList)),
|
||||
zap.Strings("whiteListIPs", whiteListIPs))
|
||||
|
||||
// 输出白名单详细信息(用于调试)
|
||||
for idx, item := range apiUser.WhiteList {
|
||||
@@ -246,24 +257,27 @@ func (s *ApiApplicationServiceImpl) validateApiCall(ctx context.Context, cmd *co
|
||||
s.logger.Error("IP不在白名单内",
|
||||
zap.String("userId", apiUser.UserId),
|
||||
zap.String("ip", cmd.ClientIP),
|
||||
zap.Int("whiteListSize", len(apiUser.WhiteList)))
|
||||
zap.Int("whiteListSize", len(apiUser.WhiteList)),
|
||||
zap.Strings("whiteListIPs", whiteListIPs))
|
||||
return nil, ErrInvalidIP
|
||||
}
|
||||
s.logger.Info("白名单验证通过", zap.String("ip", cmd.ClientIP))
|
||||
s.logger.Info("白名单验证通过",
|
||||
zap.String("ip", cmd.ClientIP),
|
||||
zap.Strings("whiteListIPs", whiteListIPs))
|
||||
}
|
||||
|
||||
// 5. 验证钱包状态
|
||||
if err := s.validateWalletStatus(ctx, apiUser.UserId, product); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 6. 验证订阅状态并获取订阅信息
|
||||
// 5. 先验证订阅(与扣费金额一致,便于余额预检使用订阅价)
|
||||
subscription, err := s.validateSubscriptionStatus(ctx, apiUser.UserId, product)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetSubscription(subscription)
|
||||
|
||||
// 6. 验证钱包状态(有订阅时按订阅价与目录价取较大者预检,避免代配价高于目录价时误判余额不足)
|
||||
if err := s.validateWalletStatus(ctx, apiUser.UserId, product, subscription); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 7. 解密参数
|
||||
requestParams, err := crypto.AesDecrypt(cmd.Data, apiUser.SecretKey)
|
||||
if err != nil {
|
||||
@@ -277,6 +291,44 @@ func (s *ApiApplicationServiceImpl) validateApiCall(ctx context.Context, cmd *co
|
||||
s.logger.Error("解析解密参数失败", zap.Error(err))
|
||||
return nil, ErrDecryptFail
|
||||
}
|
||||
|
||||
// 7.1 子账号主账号 AccessId 校验(仅在请求参数中携带时生效)
|
||||
if parentAccessID, ok := extractParentAccessID(paramsMap); ok {
|
||||
if s.subordinateRepo == nil {
|
||||
s.logger.Error("子账号主账号AccessId校验失败:subordinateRepo未初始化")
|
||||
return nil, ErrSystem
|
||||
}
|
||||
|
||||
link, err := s.subordinateRepo.FindLinkByChildUserID(ctx, apiUser.UserId)
|
||||
if err != nil {
|
||||
s.logger.Error("查询子账号主从关系失败",
|
||||
zap.String("user_id", apiUser.UserId),
|
||||
zap.Error(err))
|
||||
return nil, ErrSystem
|
||||
}
|
||||
if link == nil {
|
||||
s.logger.Warn("子账号主账号AccessId校验失败:未找到主从关系",
|
||||
zap.String("user_id", apiUser.UserId),
|
||||
zap.String("parent_access_id", parentAccessID))
|
||||
return nil, ErrQueryFailed
|
||||
}
|
||||
|
||||
parentApiUser, err := s.apiUserService.LoadApiUserByUserId(ctx, link.ParentUserID)
|
||||
if err != nil {
|
||||
s.logger.Error("加载主账号API用户失败",
|
||||
zap.String("child_user_id", apiUser.UserId),
|
||||
zap.String("parent_user_id", link.ParentUserID),
|
||||
zap.Error(err))
|
||||
return nil, ErrSystem
|
||||
}
|
||||
if parentApiUser == nil || parentApiUser.AccessId != parentAccessID {
|
||||
s.logger.Warn("子账号主账号AccessId校验失败:主账号不匹配",
|
||||
zap.String("child_user_id", apiUser.UserId),
|
||||
zap.String("parent_user_id", link.ParentUserID),
|
||||
zap.String("parent_access_id", parentAccessID))
|
||||
return nil, ErrQueryFailed
|
||||
}
|
||||
}
|
||||
result.SetRequestParams(paramsMap)
|
||||
|
||||
// 8. 获取合同信息
|
||||
@@ -293,6 +345,26 @@ func (s *ApiApplicationServiceImpl) validateApiCall(ctx context.Context, cmd *co
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// extractParentAccessID 从解密参数中提取主账号 AccessId
|
||||
// 仅支持键名:master_accessid
|
||||
func extractParentAccessID(params map[string]interface{}) (string, bool) {
|
||||
if len(params) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
value, ok := params["master_accessid"]
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
if str, ok := value.(string); ok {
|
||||
str = strings.TrimSpace(str)
|
||||
if str != "" {
|
||||
return str, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// callExternalApi 同步调用外部API
|
||||
func (s *ApiApplicationServiceImpl) callExternalApi(ctx context.Context, cmd *commands.ApiCallCommand, validation *dto.ApiCallValidationResult) (string, error) {
|
||||
// 创建CallContext
|
||||
@@ -319,15 +391,28 @@ func (s *ApiApplicationServiceImpl) callExternalApi(ctx context.Context, cmd *co
|
||||
callContext)
|
||||
|
||||
if err != nil {
|
||||
mappedErrorType := entities.ApiCallErrorSystem
|
||||
if errors.Is(err, processors.ErrDatasource) {
|
||||
return "", ErrSystem
|
||||
mappedErrorType = entities.ApiCallErrorDatasource
|
||||
} else if errors.Is(err, processors.ErrInvalidParam) {
|
||||
return "", ErrInvalidParam
|
||||
mappedErrorType = entities.ApiCallErrorInvalidParam
|
||||
} else if errors.Is(err, processors.ErrNotFound) {
|
||||
return "", ErrQueryEmpty
|
||||
} else {
|
||||
return "", ErrSystem
|
||||
mappedErrorType = entities.ApiCallErrorQueryEmpty
|
||||
}
|
||||
|
||||
s.logger.Error("调用第三方接口失败",
|
||||
zap.String("transaction_id", validation.ApiCall.TransactionId),
|
||||
zap.String("api_name", cmd.ApiName),
|
||||
zap.String("error_type", mappedErrorType),
|
||||
zap.Error(err))
|
||||
|
||||
if mappedErrorType == entities.ApiCallErrorInvalidParam {
|
||||
return "", ErrInvalidParam
|
||||
}
|
||||
if mappedErrorType == entities.ApiCallErrorQueryEmpty {
|
||||
return "", ErrQueryEmpty
|
||||
}
|
||||
return "", ErrSystem
|
||||
}
|
||||
|
||||
return string(response), nil
|
||||
@@ -1086,6 +1171,24 @@ func (s *ApiApplicationServiceImpl) ProcessDeduction(ctx context.Context, cmd *c
|
||||
return err
|
||||
}
|
||||
|
||||
// 优先扣减产品额度(若存在且可用),避免子账号有额度却因钱包余额不足失败
|
||||
deductedByQuota, err := s.tryDeductQuota(ctx, cmd.UserID, cmd.ProductID, cmd.ApiCallID, cmd.TransactionID)
|
||||
if err != nil {
|
||||
s.logger.Error("额度扣减失败",
|
||||
zap.String("transaction_id", cmd.TransactionID),
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("product_id", cmd.ProductID),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
if deductedByQuota {
|
||||
s.logger.Info("额度扣减成功",
|
||||
zap.String("transaction_id", cmd.TransactionID),
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("product_id", cmd.ProductID))
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := s.walletService.Deduct(ctx, cmd.UserID, amount, cmd.ApiCallID, cmd.TransactionID, cmd.ProductID); err != nil {
|
||||
s.logger.Error("扣款处理失败",
|
||||
zap.String("transaction_id", cmd.TransactionID),
|
||||
@@ -1179,7 +1282,26 @@ func (s *ApiApplicationServiceImpl) ProcessCompensation(ctx context.Context, cmd
|
||||
}
|
||||
|
||||
// validateWalletStatus 验证钱包状态
|
||||
func (s *ApiApplicationServiceImpl) validateWalletStatus(ctx context.Context, userID string, product *product_entities.Product) error {
|
||||
func (s *ApiApplicationServiceImpl) validateWalletStatus(ctx context.Context, userID string, product *product_entities.Product, subscription *product_entities.Subscription) error {
|
||||
// 若用户在该产品有可用额度,则本次调用将走额度扣减,不再要求钱包余额预检通过
|
||||
if s.subordinateRepo != nil {
|
||||
quotaAccount, err := s.subordinateRepo.FindQuotaAccount(ctx, userID, product.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("查询额度账户失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("product_id", product.ID),
|
||||
zap.Error(err))
|
||||
return ErrSystem
|
||||
}
|
||||
if quotaAccount != nil && quotaAccount.AvailableQuota > 0 {
|
||||
s.logger.Info("额度校验通过,跳过钱包余额预检",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("product_id", product.ID),
|
||||
zap.Int64("available_quota", quotaAccount.AvailableQuota))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 获取用户钱包信息
|
||||
wallet, err := s.walletService.LoadWalletByUserId(ctx, userID)
|
||||
if err != nil {
|
||||
@@ -1197,8 +1319,13 @@ func (s *ApiApplicationServiceImpl) validateWalletStatus(ctx context.Context, us
|
||||
return ErrFrozenAccount
|
||||
}
|
||||
|
||||
// 3. 检查钱包余额是否充足
|
||||
// 3. 检查钱包余额是否充足(有订阅时与扣费金额对齐:取目录价与订阅价较大者)
|
||||
requiredAmount := product.Price
|
||||
if subscription != nil {
|
||||
if subscription.Price.GreaterThan(requiredAmount) {
|
||||
requiredAmount = subscription.Price
|
||||
}
|
||||
}
|
||||
if wallet.Balance.LessThan(requiredAmount) {
|
||||
s.logger.Error("钱包余额不足",
|
||||
zap.String("user_id", userID),
|
||||
@@ -1224,6 +1351,56 @@ func (s *ApiApplicationServiceImpl) validateWalletStatus(ctx context.Context, us
|
||||
return nil
|
||||
}
|
||||
|
||||
// tryDeductQuota 尝试扣减产品额度;若不存在额度账户则返回 false,nil 以便回退钱包扣款
|
||||
func (s *ApiApplicationServiceImpl) tryDeductQuota(ctx context.Context, userID, productID, apiCallID, transactionID string) (bool, error) {
|
||||
if s.subordinateRepo == nil || productID == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var deducted bool
|
||||
err := s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
|
||||
account, err := s.subordinateRepo.FindQuotaAccount(txCtx, userID, productID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
if account.AvailableQuota <= 0 {
|
||||
return ErrInsufficientBalance
|
||||
}
|
||||
|
||||
before := account.AvailableQuota
|
||||
account.AvailableQuota -= 1
|
||||
account.UsedQuota += 1
|
||||
if err := s.subordinateRepo.UpdateQuotaAccount(txCtx, account); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ledger := &subordinate_entities.UserProductQuotaLedger{
|
||||
UserID: userID,
|
||||
ProductID: productID,
|
||||
ChangeType: subordinate_entities.QuotaLedgerChangeTypeConsumeAPI,
|
||||
DeltaQuota: -1,
|
||||
BeforeQuota: before,
|
||||
AfterQuota: account.AvailableQuota,
|
||||
SourceID: apiCallID,
|
||||
OperatorID: userID,
|
||||
Remark: fmt.Sprintf("API调用扣减,transaction_id=%s", transactionID),
|
||||
}
|
||||
if err := s.subordinateRepo.CreateQuotaLedger(txCtx, ledger); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deducted = true
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return deducted, nil
|
||||
}
|
||||
|
||||
// validateSubscriptionStatus 验证订阅状态并返回订阅信息
|
||||
func (s *ApiApplicationServiceImpl) validateSubscriptionStatus(ctx context.Context, userID string, product *product_entities.Product) (*product_entities.Subscription, error) {
|
||||
// 1. 检查用户是否已订阅该产品
|
||||
|
||||
@@ -5,6 +5,7 @@ import "errors"
|
||||
// API调用相关错误类型
|
||||
var (
|
||||
ErrQueryEmpty = errors.New("查询为空")
|
||||
ErrQueryFailed = errors.New("查询失败")
|
||||
ErrSystem = errors.New("接口异常")
|
||||
ErrDecryptFail = errors.New("解密失败")
|
||||
ErrRequestParam = errors.New("请求参数结构不正确")
|
||||
@@ -27,6 +28,7 @@ var (
|
||||
// 错误码映射 - 严格按照用户要求
|
||||
var ErrorCodeMap = map[error]int{
|
||||
ErrQueryEmpty: 1000,
|
||||
ErrQueryFailed: 1000,
|
||||
ErrSystem: 1001,
|
||||
ErrDecryptFail: 1002,
|
||||
ErrRequestParam: 1003,
|
||||
|
||||
@@ -37,6 +37,17 @@ type CertificationApplicationService interface {
|
||||
// AdminCompleteCertificationWithoutContract 管理员代用户完成认证(暂不关联合同)
|
||||
AdminCompleteCertificationWithoutContract(ctx context.Context, cmd *commands.AdminCompleteCertificationCommand) (*responses.CertificationResponse, error)
|
||||
|
||||
// AdminListSubmitRecords 管理端分页查询企业信息提交记录
|
||||
AdminListSubmitRecords(ctx context.Context, query *queries.AdminListSubmitRecordsQuery) (*responses.AdminSubmitRecordsListResponse, error)
|
||||
// AdminGetSubmitRecordByID 管理端获取单条提交记录详情
|
||||
AdminGetSubmitRecordByID(ctx context.Context, recordID string) (*responses.AdminSubmitRecordDetail, error)
|
||||
// AdminApproveSubmitRecord 管理端审核通过(按提交记录 ID)
|
||||
AdminApproveSubmitRecord(ctx context.Context, recordID, adminID, remark string) error
|
||||
// AdminRejectSubmitRecord 管理端审核拒绝(按提交记录 ID)
|
||||
AdminRejectSubmitRecord(ctx context.Context, recordID, adminID, remark string) error
|
||||
// AdminTransitionCertificationStatus 管理端按用户变更认证状态(以状态机为准:info_submitted=通过 / info_rejected=拒绝)
|
||||
AdminTransitionCertificationStatus(ctx context.Context, cmd *commands.AdminTransitionCertificationStatusCommand) error
|
||||
|
||||
// ================ e签宝回调处理 ================
|
||||
|
||||
// 处理e签宝回调
|
||||
|
||||
@@ -2,24 +2,30 @@ package certification
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"tyapi-server/internal/application/certification/dto/commands"
|
||||
"tyapi-server/internal/application/certification/dto/queries"
|
||||
"tyapi-server/internal/application/certification/dto/responses"
|
||||
"tyapi-server/internal/config"
|
||||
api_service "tyapi-server/internal/domains/api/services"
|
||||
"tyapi-server/internal/domains/certification/entities"
|
||||
certification_value_objects "tyapi-server/internal/domains/certification/entities/value_objects"
|
||||
"tyapi-server/internal/domains/certification/enums"
|
||||
"tyapi-server/internal/domains/certification/repositories"
|
||||
finance_entities "tyapi-server/internal/domains/finance/entities"
|
||||
finance_repositories "tyapi-server/internal/domains/finance/repositories"
|
||||
"tyapi-server/internal/domains/certification/services"
|
||||
subordinate_repositories "tyapi-server/internal/domains/subordinate/repositories"
|
||||
finance_service "tyapi-server/internal/domains/finance/services"
|
||||
user_entities "tyapi-server/internal/domains/user/entities"
|
||||
user_service "tyapi-server/internal/domains/user/services"
|
||||
"tyapi-server/internal/config"
|
||||
user_service "tyapi-server/internal/domains/user/services"
|
||||
"tyapi-server/internal/infrastructure/external/notification"
|
||||
"tyapi-server/internal/infrastructure/external/storage"
|
||||
"tyapi-server/internal/shared/database"
|
||||
@@ -47,10 +53,13 @@ type CertificationApplicationServiceImpl struct {
|
||||
// 仓储依赖
|
||||
queryRepository repositories.CertificationQueryRepository
|
||||
enterpriseInfoSubmitRecordRepo repositories.EnterpriseInfoSubmitRecordRepository
|
||||
subordinateRepo subordinate_repositories.SubordinateRepository
|
||||
walletRepo finance_repositories.WalletRepository
|
||||
txManager *database.TransactionManager
|
||||
|
||||
wechatWorkService *notification.WeChatWorkService
|
||||
logger *zap.Logger
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
// NewCertificationApplicationService 创建认证应用服务
|
||||
@@ -68,6 +77,8 @@ func NewCertificationApplicationService(
|
||||
apiUserAggregateService api_service.ApiUserAggregateService,
|
||||
enterpriseInfoSubmitRecordService *services.EnterpriseInfoSubmitRecordService,
|
||||
ocrService sharedOCR.OCRService,
|
||||
subordinateRepo subordinate_repositories.SubordinateRepository,
|
||||
walletRepo finance_repositories.WalletRepository,
|
||||
txManager *database.TransactionManager,
|
||||
logger *zap.Logger,
|
||||
cfg *config.Config,
|
||||
@@ -90,9 +101,12 @@ func NewCertificationApplicationService(
|
||||
apiUserAggregateService: apiUserAggregateService,
|
||||
enterpriseInfoSubmitRecordService: enterpriseInfoSubmitRecordService,
|
||||
ocrService: ocrService,
|
||||
subordinateRepo: subordinateRepo,
|
||||
walletRepo: walletRepo,
|
||||
txManager: txManager,
|
||||
wechatWorkService: wechatSvc,
|
||||
logger: logger,
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,9 +118,55 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
|
||||
cmd *commands.SubmitEnterpriseInfoCommand,
|
||||
) (*responses.CertificationResponse, error) {
|
||||
s.logger.Info("开始提交企业信息",
|
||||
zap.String("user_id", cmd.UserID))
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("company_name", cmd.CompanyName),
|
||||
zap.String("unified_social_code", cmd.UnifiedSocialCode))
|
||||
|
||||
// 1.5 插入企业信息提交记录
|
||||
// 0. 若该用户已有待审核(认证状态仍在待审核),则不允许重复提交
|
||||
latestRecord, err := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(ctx, cmd.UserID)
|
||||
if err == nil && latestRecord != nil {
|
||||
s.logger.Info("步骤0-检测到历史提交记录",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("latest_record_id", latestRecord.ID))
|
||||
cert, loadErr := s.aggregateService.LoadCertificationByUserID(ctx, cmd.UserID)
|
||||
if loadErr == nil && cert != nil && cert.Status == enums.StatusInfoPendingReview {
|
||||
s.logger.Warn("步骤0-存在待审核记录,拒绝重复提交",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("cert_status", string(cert.Status)))
|
||||
return nil, fmt.Errorf("您已有待审核的提交,请等待管理员审核后再操作")
|
||||
}
|
||||
}
|
||||
|
||||
// 0.5 已通过人工审核或已进入后续流程:幂等返回当前认证数据(不调 e签宝、不新建提交记录)
|
||||
existsCertEarly, err := s.aggregateService.ExistsByUserID(ctx, cmd.UserID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("检查认证记录失败: %w", err)
|
||||
}
|
||||
if existsCertEarly {
|
||||
certEarly, loadErr := s.aggregateService.LoadCertificationByUserID(ctx, cmd.UserID)
|
||||
if loadErr != nil {
|
||||
return nil, fmt.Errorf("加载认证信息失败: %w", loadErr)
|
||||
}
|
||||
switch certEarly.Status {
|
||||
case enums.StatusInfoSubmitted, enums.StatusEnterpriseVerified, enums.StatusContractApplied,
|
||||
enums.StatusContractSigned, enums.StatusCompleted, enums.StatusContractRejected, enums.StatusContractExpired:
|
||||
meta, metaErr := s.AddStatusMetadata(ctx, certEarly)
|
||||
if metaErr != nil {
|
||||
return nil, metaErr
|
||||
}
|
||||
resp := s.convertToResponse(certEarly)
|
||||
if meta != nil {
|
||||
resp.Metadata = meta
|
||||
} else {
|
||||
resp.Metadata = map[string]interface{}{}
|
||||
}
|
||||
resp.Metadata["next_action"] = enums.GetUserActionHint(certEarly.Status)
|
||||
s.logger.Info("企业信息提交幂等返回", zap.String("user_id", cmd.UserID), zap.String("status", string(certEarly.Status)))
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 1.5 插入企业信息提交记录(包含扩展字段)
|
||||
record := entities.NewEnterpriseInfoSubmitRecord(
|
||||
cmd.UserID,
|
||||
cmd.CompanyName,
|
||||
@@ -117,10 +177,44 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
|
||||
cmd.EnterpriseAddress,
|
||||
)
|
||||
|
||||
// 扩展字段赋值
|
||||
record.BusinessLicenseImageURL = cmd.BusinessLicenseImageURL
|
||||
if len(cmd.OfficePlaceImageURLs) > 0 {
|
||||
if data, mErr := json.Marshal(cmd.OfficePlaceImageURLs); mErr == nil {
|
||||
record.OfficePlaceImageURLs = string(data)
|
||||
} else {
|
||||
s.logger.Warn("序列化办公场地图片URL失败", zap.Error(mErr))
|
||||
}
|
||||
}
|
||||
|
||||
record.APIUsage = cmd.APIUsage
|
||||
if len(cmd.ScenarioAttachmentURLs) > 0 {
|
||||
if data, mErr := json.Marshal(cmd.ScenarioAttachmentURLs); mErr == nil {
|
||||
record.ScenarioAttachmentURLs = string(data)
|
||||
} else {
|
||||
s.logger.Warn("序列化场景附件图片URL失败", zap.Error(mErr))
|
||||
}
|
||||
}
|
||||
// 授权代表信息落库
|
||||
record.AuthorizedRepName = cmd.AuthorizedRepName
|
||||
record.AuthorizedRepID = cmd.AuthorizedRepID
|
||||
record.AuthorizedRepPhone = cmd.AuthorizedRepPhone
|
||||
if len(cmd.AuthorizedRepIDImageURLs) > 0 {
|
||||
if data, mErr := json.Marshal(cmd.AuthorizedRepIDImageURLs); mErr == nil {
|
||||
record.AuthorizedRepIDImageURLs = string(data)
|
||||
} else {
|
||||
s.logger.Warn("序列化授权代表身份证图片URL失败", zap.Error(mErr))
|
||||
}
|
||||
}
|
||||
|
||||
// 验证验证码
|
||||
// 特殊验证码"768005"直接跳过验证环节
|
||||
if cmd.VerificationCode != "768005" {
|
||||
s.logger.Info("步骤1-开始验证短信验证码", zap.String("user_id", cmd.UserID))
|
||||
if err := s.smsCodeService.VerifyCode(ctx, cmd.LegalPersonPhone, cmd.VerificationCode, user_entities.SMSSceneCertification); err != nil {
|
||||
s.logger.Warn("步骤1-短信验证码校验失败",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.Error(err))
|
||||
record.MarkAsFailed(err.Error())
|
||||
saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record)
|
||||
if saveErr != nil {
|
||||
@@ -128,12 +222,20 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
|
||||
}
|
||||
return nil, fmt.Errorf("验证码错误或已过期")
|
||||
}
|
||||
s.logger.Info("步骤1-短信验证码校验通过", zap.String("user_id", cmd.UserID))
|
||||
} else {
|
||||
s.logger.Info("步骤1-命中特殊验证码,跳过校验", zap.String("user_id", cmd.UserID))
|
||||
}
|
||||
s.logger.Info("开始处理企业信息提交",
|
||||
zap.String("user_id", cmd.UserID))
|
||||
// 1. 检查企业信息是否重复(统一社会信用代码,已经认证了的,不能重复提交)
|
||||
// 1. 检查企业信息是否重复(统一社会信用代码:已认证或已提交待审核的都不能重复)
|
||||
// 1.1 已写入用户域 enterprise_infos 的(已完成认证)
|
||||
exists, err := s.userAggregateService.CheckUnifiedSocialCodeExists(ctx, cmd.UnifiedSocialCode, cmd.UserID)
|
||||
if err != nil {
|
||||
s.logger.Error("步骤2.1-检查用户域统一社会信用代码失败",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("unified_social_code", cmd.UnifiedSocialCode),
|
||||
zap.Error(err))
|
||||
record.MarkAsFailed(err.Error())
|
||||
saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record)
|
||||
if saveErr != nil {
|
||||
@@ -142,6 +244,34 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
|
||||
return nil, fmt.Errorf("检查企业信息失败: %s", err.Error())
|
||||
}
|
||||
if exists {
|
||||
s.logger.Warn("步骤2.1-统一社会信用代码已被占用(用户域)",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("unified_social_code", cmd.UnifiedSocialCode))
|
||||
record.MarkAsFailed("该企业信息已被其他用户使用,请确认企业信息是否正确")
|
||||
saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record)
|
||||
if saveErr != nil {
|
||||
return nil, fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error())
|
||||
}
|
||||
return nil, fmt.Errorf("该企业信息已被其他用户使用,请确认企业信息是否正确")
|
||||
}
|
||||
// 1.2 已提交/已通过验证的提交记录(尚未完成认证但已占用的信用代码)
|
||||
existsInSubmit, err := s.enterpriseInfoSubmitRecordRepo.ExistsByUnifiedSocialCodeExcludeUser(ctx, cmd.UnifiedSocialCode, cmd.UserID)
|
||||
if err != nil {
|
||||
s.logger.Error("步骤2.2-检查提交记录统一社会信用代码失败",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("unified_social_code", cmd.UnifiedSocialCode),
|
||||
zap.Error(err))
|
||||
record.MarkAsFailed(err.Error())
|
||||
saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record)
|
||||
if saveErr != nil {
|
||||
return nil, fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error())
|
||||
}
|
||||
return nil, fmt.Errorf("检查企业信息失败: %s", err.Error())
|
||||
}
|
||||
if existsInSubmit {
|
||||
s.logger.Warn("步骤2.2-统一社会信用代码已被占用(提交记录)",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("unified_social_code", cmd.UnifiedSocialCode))
|
||||
record.MarkAsFailed("该企业信息已被其他用户使用,请确认企业信息是否正确")
|
||||
saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record)
|
||||
if saveErr != nil {
|
||||
@@ -168,6 +298,9 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
|
||||
}
|
||||
return nil, fmt.Errorf("企业信息验证失败: %s", err.Error())
|
||||
}
|
||||
s.logger.Info("步骤3-企业信息基础校验通过",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("company_name", enterpriseInfo.CompanyName))
|
||||
err = s.enterpriseInfoSubmitRecordService.ValidateWithWestdex(ctx, enterpriseInfo)
|
||||
if err != nil {
|
||||
s.logger.Error("企业信息验证失败", zap.Error(err))
|
||||
@@ -178,14 +311,14 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
|
||||
}
|
||||
return nil, fmt.Errorf("企业信息验证失败, %s", err.Error())
|
||||
}
|
||||
s.logger.Info("步骤4-企业信息三方校验通过",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("company_name", enterpriseInfo.CompanyName))
|
||||
record.MarkAsVerified()
|
||||
saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record)
|
||||
if saveErr != nil {
|
||||
return nil, fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error())
|
||||
}
|
||||
|
||||
var response *responses.CertificationResponse
|
||||
err = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
|
||||
s.logger.Info("步骤5-开始事务处理认证提交流程", zap.String("user_id", cmd.UserID))
|
||||
// 2. 检查用户认证是否存在
|
||||
existsCert, err := s.aggregateService.ExistsByUserID(txCtx, cmd.UserID)
|
||||
if err != nil {
|
||||
@@ -193,10 +326,12 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
|
||||
}
|
||||
if !existsCert {
|
||||
// 创建
|
||||
s.logger.Info("步骤5.1-认证记录不存在,开始创建", zap.String("user_id", cmd.UserID))
|
||||
_, err := s.aggregateService.CreateCertification(txCtx, cmd.UserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建认证信息失败: %s", err.Error())
|
||||
}
|
||||
s.logger.Info("步骤5.1-认证记录创建成功", zap.String("user_id", cmd.UserID))
|
||||
}
|
||||
|
||||
// 3. 加载认证聚合根
|
||||
@@ -205,80 +340,91 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
|
||||
return fmt.Errorf("加载认证信息失败: %s", err.Error())
|
||||
}
|
||||
|
||||
// 3. 调用e签宝看是否进行过认证
|
||||
respMeta := map[string]interface{}{}
|
||||
|
||||
identity, err := s.esignClient.QueryOrgIdentityInfo(&esign.QueryOrgIdentityRequest{
|
||||
OrgName: cmd.CompanyName,
|
||||
})
|
||||
if identity != nil && identity.Data.RealnameStatus == 1 {
|
||||
// 已提交
|
||||
err = cert.SubmitEnterpriseInfo(enterpriseInfo, "", "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("提交企业认证信息失败: %s", err.Error())
|
||||
}
|
||||
s.logger.Info("企业认证成功", zap.Any("identity", identity))
|
||||
|
||||
// 完成企业认证流程
|
||||
err = s.completeEnterpriseVerification(txCtx, cert, cmd.UserID, cmd.CompanyName, cmd.LegalPersonName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
respMeta = map[string]interface{}{
|
||||
"enterprise_info": enterpriseInfo,
|
||||
"next_action": "企业已认证,可进行后续操作",
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
s.logger.Error("e签宝查询企业认证信息失败或未进行企业认证", zap.Error(err))
|
||||
}
|
||||
authURL, err := s.esignClient.GenerateEnterpriseAuth(&esign.EnterpriseAuthRequest{
|
||||
CompanyName: enterpriseInfo.CompanyName,
|
||||
UnifiedSocialCode: enterpriseInfo.UnifiedSocialCode,
|
||||
LegalPersonName: enterpriseInfo.LegalPersonName,
|
||||
LegalPersonID: enterpriseInfo.LegalPersonID,
|
||||
TransactorName: enterpriseInfo.LegalPersonName,
|
||||
TransactorMobile: enterpriseInfo.LegalPersonPhone,
|
||||
TransactorID: enterpriseInfo.LegalPersonID,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("生成企业认证链接失败", zap.Error(err))
|
||||
return fmt.Errorf("生成企业认证链接失败: %s", err.Error())
|
||||
}
|
||||
err = cert.SubmitEnterpriseInfo(enterpriseInfo, authURL.AuthShortURL, authURL.AuthFlowID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("提交企业认证信息失败: %s", err.Error())
|
||||
}
|
||||
respMeta = map[string]interface{}{
|
||||
"enterprise_info": enterpriseInfo,
|
||||
"authUrl": authURL.AuthURL,
|
||||
"next_action": "请完成企业认证",
|
||||
}
|
||||
// 4. 提交企业信息:进入人工审核(三真/企业信息审核);e签宝链接仅在管理员审核通过后生成(见 AdminApproveSubmitRecord)
|
||||
if err := cert.SubmitEnterpriseInfoForReview(enterpriseInfo); err != nil {
|
||||
return fmt.Errorf("提交企业信息失败: %s", err.Error())
|
||||
}
|
||||
|
||||
err = s.aggregateService.SaveCertification(txCtx, cert)
|
||||
if err != nil {
|
||||
if err := s.aggregateService.SaveCertification(txCtx, cert); err != nil {
|
||||
return fmt.Errorf("保存认证信息失败: %s", err.Error())
|
||||
}
|
||||
// 5. 转换为响应DTO
|
||||
response = s.convertToResponse(cert)
|
||||
|
||||
// 6. 添加工作流结果信息
|
||||
if respMeta != nil {
|
||||
response.Metadata = respMeta
|
||||
// 5. 提交记录与认证状态在同一事务内保存
|
||||
if saveErr := s.enterpriseInfoSubmitRecordService.Save(txCtx, record); saveErr != nil {
|
||||
return fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error())
|
||||
}
|
||||
s.logger.Info("步骤5.3-企业信息提交记录保存成功",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("record_id", record.ID))
|
||||
|
||||
var enterpriseInfoMeta map[string]interface{}
|
||||
if raw, mErr := json.Marshal(enterpriseInfo); mErr == nil {
|
||||
_ = json.Unmarshal(raw, &enterpriseInfoMeta)
|
||||
}
|
||||
if enterpriseInfoMeta == nil {
|
||||
enterpriseInfoMeta = map[string]interface{}{}
|
||||
}
|
||||
enterpriseInfoMeta["submit_at"] = record.SubmitAt.Format(time.RFC3339)
|
||||
|
||||
respMeta := map[string]interface{}{
|
||||
"enterprise_info": enterpriseInfoMeta,
|
||||
"polling": map[string]interface{}{
|
||||
"enabled": false,
|
||||
"endpoint": "/api/v1/certifications/confirm-auth",
|
||||
"interval_seconds": 3,
|
||||
},
|
||||
"next_action": "请等待管理员审核企业信息",
|
||||
"target_view": "manual_review",
|
||||
}
|
||||
// 6. 转换为响应 DTO
|
||||
response = s.convertToResponse(cert)
|
||||
response.Metadata = respMeta
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 提醒管理员处理待审核申请(配置企业微信 Webhook 时生效)
|
||||
if s.wechatWorkService != nil {
|
||||
contactPhone := cmd.LegalPersonPhone
|
||||
if strings.TrimSpace(cmd.AuthorizedRepPhone) != "" {
|
||||
contactPhone = fmt.Sprintf("法人 %s;授权代表 %s", cmd.LegalPersonPhone, cmd.AuthorizedRepPhone)
|
||||
} else {
|
||||
contactPhone = fmt.Sprintf("%s(法人)", cmd.LegalPersonPhone)
|
||||
}
|
||||
_ = s.wechatWorkService.SendCertificationNotification(ctx, "pending_manual_review", map[string]interface{}{
|
||||
"company_name": cmd.CompanyName,
|
||||
"legal_person_name": cmd.LegalPersonName,
|
||||
"authorized_rep_name": cmd.AuthorizedRepName,
|
||||
"contact_phone": contactPhone,
|
||||
"api_usage": cmd.APIUsage,
|
||||
"submit_at": record.SubmitAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
|
||||
s.logger.Info("企业信息提交成功", zap.String("user_id", cmd.UserID))
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// 审核状态检查(步骤二)
|
||||
// 规则:企业信息提交成功后进入待审核;审核通过后才允许进行企业认证确认(ConfirmAuth)。
|
||||
func (s *CertificationApplicationServiceImpl) checkAuditStatus(ctx context.Context, cert *entities.Certification) error {
|
||||
switch cert.Status {
|
||||
case enums.StatusInfoSubmitted,
|
||||
enums.StatusEnterpriseVerified,
|
||||
enums.StatusContractApplied,
|
||||
enums.StatusContractSigned,
|
||||
enums.StatusCompleted:
|
||||
return nil
|
||||
case enums.StatusInfoPendingReview:
|
||||
return fmt.Errorf("企业信息已提交,正在审核中")
|
||||
case enums.StatusInfoRejected:
|
||||
return fmt.Errorf("企业信息审核未通过")
|
||||
default:
|
||||
return fmt.Errorf("认证状态不正确,当前状态: %s", enums.GetStatusName(cert.Status))
|
||||
}
|
||||
}
|
||||
|
||||
// ConfirmAuth 确认认证状态
|
||||
func (s *CertificationApplicationServiceImpl) ConfirmAuth(
|
||||
ctx context.Context,
|
||||
@@ -290,14 +436,24 @@ func (s *CertificationApplicationServiceImpl) ConfirmAuth(
|
||||
return nil, fmt.Errorf("加载认证信息失败: %s", err.Error())
|
||||
}
|
||||
|
||||
// 企业认证
|
||||
if cert.Status != enums.StatusInfoSubmitted {
|
||||
return nil, fmt.Errorf("认证状态不正确,当前状态: %s", enums.GetStatusName(cert.Status))
|
||||
// 步骤二:审核状态检查(审核通过后才能进入企业认证确认)
|
||||
s.logger.Info("确认状态-步骤1-开始审核状态检查", zap.String("user_id", cmd.UserID))
|
||||
if err := s.checkAuditStatus(ctx, cert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.logger.Info("确认状态-步骤1-审核状态检查通过",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("cert_status", string(cert.Status)))
|
||||
record, err := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(ctx, cert.UserID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查找企业信息失败: %w", err)
|
||||
}
|
||||
s.logger.Info("确认状态-步骤2-获取最近提交记录成功",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("record_id", record.ID))
|
||||
s.logger.Info("确认状态-步骤3-开始查询三方实名状态",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("company_name", record.CompanyName))
|
||||
identity, err := s.esignClient.QueryOrgIdentityInfo(&esign.QueryOrgIdentityRequest{
|
||||
OrgName: record.CompanyName,
|
||||
})
|
||||
@@ -307,6 +463,8 @@ func (s *CertificationApplicationServiceImpl) ConfirmAuth(
|
||||
}
|
||||
reason := ""
|
||||
if identity != nil && identity.Data.RealnameStatus == 1 {
|
||||
s.logger.Info("确认状态-步骤3-三方实名状态已完成,准备事务内推进认证",
|
||||
zap.String("user_id", cmd.UserID))
|
||||
err = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
|
||||
err = s.completeEnterpriseVerification(txCtx, cert, cert.UserID, record.CompanyName, record.LegalPersonName)
|
||||
if err != nil {
|
||||
@@ -318,8 +476,13 @@ func (s *CertificationApplicationServiceImpl) ConfirmAuth(
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("完成企业认证失败: %w", err)
|
||||
}
|
||||
s.logger.Info("确认状态-步骤4-认证状态推进完成",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("cert_status", string(cert.Status)))
|
||||
} else {
|
||||
reason = "企业未完成"
|
||||
s.logger.Info("确认状态-步骤3-三方实名状态未完成",
|
||||
zap.String("user_id", cmd.UserID))
|
||||
}
|
||||
return &responses.ConfirmAuthResponse{
|
||||
Status: cert.Status,
|
||||
@@ -694,6 +857,310 @@ func (s *CertificationApplicationServiceImpl) AdminCompleteCertificationWithoutC
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// AdminListSubmitRecords 管理端分页查询企业信息提交记录
|
||||
func (s *CertificationApplicationServiceImpl) AdminListSubmitRecords(
|
||||
ctx context.Context,
|
||||
query *queries.AdminListSubmitRecordsQuery,
|
||||
) (*responses.AdminSubmitRecordsListResponse, error) {
|
||||
if query.PageSize <= 0 {
|
||||
query.PageSize = 10
|
||||
}
|
||||
if query.Page <= 0 {
|
||||
query.Page = 1
|
||||
}
|
||||
filter := repositories.ListSubmitRecordsFilter{
|
||||
Page: query.Page,
|
||||
PageSize: query.PageSize,
|
||||
CertificationStatus: query.CertificationStatus,
|
||||
CompanyName: query.CompanyName,
|
||||
LegalPersonPhone: query.LegalPersonPhone,
|
||||
LegalPersonName: query.LegalPersonName,
|
||||
}
|
||||
result, err := s.enterpriseInfoSubmitRecordRepo.List(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询提交记录失败: %w", err)
|
||||
}
|
||||
items := make([]*responses.AdminSubmitRecordItem, 0, len(result.Records))
|
||||
for _, r := range result.Records {
|
||||
certStatus := ""
|
||||
if cert, err := s.aggregateService.LoadCertificationByUserID(ctx, r.UserID); err == nil && cert != nil {
|
||||
certStatus = string(cert.Status)
|
||||
}
|
||||
items = append(items, &responses.AdminSubmitRecordItem{
|
||||
ID: r.ID,
|
||||
UserID: r.UserID,
|
||||
CompanyName: r.CompanyName,
|
||||
UnifiedSocialCode: r.UnifiedSocialCode,
|
||||
LegalPersonName: r.LegalPersonName,
|
||||
SubmitAt: r.SubmitAt,
|
||||
Status: r.Status,
|
||||
CertificationStatus: certStatus,
|
||||
})
|
||||
}
|
||||
totalPages := int((result.Total + int64(query.PageSize) - 1) / int64(query.PageSize))
|
||||
if totalPages == 0 {
|
||||
totalPages = 1
|
||||
}
|
||||
return &responses.AdminSubmitRecordsListResponse{
|
||||
Items: items,
|
||||
Total: result.Total,
|
||||
Page: query.Page,
|
||||
PageSize: query.PageSize,
|
||||
TotalPages: totalPages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AdminGetSubmitRecordByID 管理端获取单条提交记录详情
|
||||
func (s *CertificationApplicationServiceImpl) AdminGetSubmitRecordByID(ctx context.Context, recordID string) (*responses.AdminSubmitRecordDetail, error) {
|
||||
record, err := s.enterpriseInfoSubmitRecordRepo.FindByID(ctx, recordID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取提交记录失败: %w", err)
|
||||
}
|
||||
certStatus := ""
|
||||
if cert, loadErr := s.aggregateService.LoadCertificationByUserID(ctx, record.UserID); loadErr == nil && cert != nil {
|
||||
certStatus = string(cert.Status)
|
||||
}
|
||||
return &responses.AdminSubmitRecordDetail{
|
||||
ID: record.ID,
|
||||
UserID: record.UserID,
|
||||
CompanyName: record.CompanyName,
|
||||
UnifiedSocialCode: record.UnifiedSocialCode,
|
||||
LegalPersonName: record.LegalPersonName,
|
||||
LegalPersonID: record.LegalPersonID,
|
||||
LegalPersonPhone: record.LegalPersonPhone,
|
||||
EnterpriseAddress: record.EnterpriseAddress,
|
||||
AuthorizedRepName: record.AuthorizedRepName,
|
||||
AuthorizedRepID: record.AuthorizedRepID,
|
||||
AuthorizedRepPhone: record.AuthorizedRepPhone,
|
||||
AuthorizedRepIDImageURLs: record.AuthorizedRepIDImageURLs,
|
||||
BusinessLicenseImageURL: record.BusinessLicenseImageURL,
|
||||
OfficePlaceImageURLs: record.OfficePlaceImageURLs,
|
||||
APIUsage: record.APIUsage,
|
||||
ScenarioAttachmentURLs: record.ScenarioAttachmentURLs,
|
||||
Status: record.Status,
|
||||
SubmitAt: record.SubmitAt,
|
||||
VerifiedAt: record.VerifiedAt,
|
||||
FailedAt: record.FailedAt,
|
||||
FailureReason: record.FailureReason,
|
||||
CertificationStatus: certStatus,
|
||||
CreatedAt: record.CreatedAt,
|
||||
UpdatedAt: record.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AdminApproveSubmitRecord 管理端审核通过
|
||||
func (s *CertificationApplicationServiceImpl) AdminApproveSubmitRecord(ctx context.Context, recordID, adminID, remark string) error {
|
||||
record, err := s.enterpriseInfoSubmitRecordRepo.FindByID(ctx, recordID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取提交记录失败: %w", err)
|
||||
}
|
||||
if record.Status != "verified" {
|
||||
return fmt.Errorf("该条提交记录未通过前置校验或已失败,无法审核通过")
|
||||
}
|
||||
cert, err := s.aggregateService.LoadCertificationByUserID(ctx, record.UserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("加载认证信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 幂等:认证已进入「已提交企业信息」或更后续状态,说明已通过审核,无需重复操作
|
||||
switch cert.Status {
|
||||
case enums.StatusInfoSubmitted,
|
||||
enums.StatusEnterpriseVerified,
|
||||
enums.StatusContractApplied,
|
||||
enums.StatusContractSigned,
|
||||
enums.StatusCompleted,
|
||||
enums.StatusContractRejected,
|
||||
enums.StatusContractExpired:
|
||||
return nil
|
||||
}
|
||||
|
||||
if cert.Status != enums.StatusInfoPendingReview {
|
||||
return fmt.Errorf("认证状态不是待审核,当前: %s", enums.GetStatusName(cert.Status))
|
||||
}
|
||||
enterpriseInfo := &certification_value_objects.EnterpriseInfo{
|
||||
CompanyName: record.CompanyName,
|
||||
UnifiedSocialCode: record.UnifiedSocialCode,
|
||||
LegalPersonName: record.LegalPersonName,
|
||||
LegalPersonID: record.LegalPersonID,
|
||||
LegalPersonPhone: record.LegalPersonPhone,
|
||||
EnterpriseAddress: record.EnterpriseAddress,
|
||||
}
|
||||
authReq := &esign.EnterpriseAuthRequest{
|
||||
CompanyName: enterpriseInfo.CompanyName,
|
||||
UnifiedSocialCode: enterpriseInfo.UnifiedSocialCode,
|
||||
LegalPersonName: enterpriseInfo.LegalPersonName,
|
||||
LegalPersonID: enterpriseInfo.LegalPersonID,
|
||||
TransactorName: enterpriseInfo.LegalPersonName,
|
||||
TransactorMobile: enterpriseInfo.LegalPersonPhone,
|
||||
TransactorID: enterpriseInfo.LegalPersonID,
|
||||
}
|
||||
authURL, alreadyVerified, err := s.generateEnterpriseAuthOrDetectVerified(ctx, authReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("生成企业认证链接失败: %w", err)
|
||||
}
|
||||
if alreadyVerified {
|
||||
if err := cert.ApproveEnterpriseInfoReview("", "", adminID); err != nil {
|
||||
return fmt.Errorf("更新认证状态失败: %w", err)
|
||||
}
|
||||
if err := s.completeEnterpriseVerification(ctx, cert, cert.UserID, record.CompanyName, record.LegalPersonName); err != nil {
|
||||
return err
|
||||
}
|
||||
record.MarkManualApproved(adminID, remark)
|
||||
if err := s.enterpriseInfoSubmitRecordService.Save(ctx, record); err != nil {
|
||||
return fmt.Errorf("保存企业信息提交记录失败: %w", err)
|
||||
}
|
||||
s.logger.Info("管理员审核通过企业信息", zap.String("record_id", recordID), zap.String("admin_id", adminID))
|
||||
return nil
|
||||
}
|
||||
if err := cert.ApproveEnterpriseInfoReview(authURL.AuthShortURL, authURL.AuthFlowID, adminID); err != nil {
|
||||
return fmt.Errorf("更新认证状态失败: %w", err)
|
||||
}
|
||||
if err := s.aggregateService.SaveCertification(ctx, cert); err != nil {
|
||||
return fmt.Errorf("保存认证信息失败: %w", err)
|
||||
}
|
||||
record.MarkManualApproved(adminID, remark)
|
||||
if err := s.enterpriseInfoSubmitRecordService.Save(ctx, record); err != nil {
|
||||
return fmt.Errorf("保存企业信息提交记录失败: %w", err)
|
||||
}
|
||||
s.logger.Info("管理员审核通过企业信息", zap.String("record_id", recordID), zap.String("admin_id", adminID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// AdminRejectSubmitRecord 管理端审核拒绝
|
||||
func (s *CertificationApplicationServiceImpl) AdminRejectSubmitRecord(ctx context.Context, recordID, adminID, remark string) error {
|
||||
if remark == "" {
|
||||
return fmt.Errorf("拒绝时必须填写审核备注")
|
||||
}
|
||||
record, err := s.enterpriseInfoSubmitRecordRepo.FindByID(ctx, recordID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取提交记录失败: %w", err)
|
||||
}
|
||||
if record.Status != "verified" {
|
||||
return fmt.Errorf("该条提交记录未通过前置校验或已失败,无法从后台拒绝(请查看历史失败原因)")
|
||||
}
|
||||
cert, err := s.aggregateService.LoadCertificationByUserID(ctx, record.UserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("加载认证信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 幂等:认证已处于拒绝或后续状态,无需重复拒绝
|
||||
switch cert.Status {
|
||||
case enums.StatusInfoRejected,
|
||||
enums.StatusEnterpriseVerified,
|
||||
enums.StatusContractApplied,
|
||||
enums.StatusContractSigned,
|
||||
enums.StatusCompleted,
|
||||
enums.StatusContractRejected,
|
||||
enums.StatusContractExpired:
|
||||
return nil
|
||||
}
|
||||
if cert.Status != enums.StatusInfoPendingReview {
|
||||
return fmt.Errorf("认证状态不是待审核,当前: %s", enums.GetStatusName(cert.Status))
|
||||
}
|
||||
if err := cert.RejectEnterpriseInfoReview(adminID, remark); err != nil {
|
||||
return fmt.Errorf("更新认证状态失败: %w", err)
|
||||
}
|
||||
if err := s.aggregateService.SaveCertification(ctx, cert); err != nil {
|
||||
return fmt.Errorf("保存认证信息失败: %w", err)
|
||||
}
|
||||
record.MarkManualRejected(adminID, remark)
|
||||
if err := s.enterpriseInfoSubmitRecordService.Save(ctx, record); err != nil {
|
||||
return fmt.Errorf("保存企业信息提交记录失败: %w", err)
|
||||
}
|
||||
s.logger.Info("管理员审核拒绝企业信息", zap.String("record_id", recordID), zap.String("admin_id", adminID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// AdminTransitionCertificationStatus 管理端按用户变更认证状态(以状态机为准)
|
||||
func (s *CertificationApplicationServiceImpl) AdminTransitionCertificationStatus(ctx context.Context, cmd *commands.AdminTransitionCertificationStatusCommand) error {
|
||||
cert, err := s.aggregateService.LoadCertificationByUserID(ctx, cmd.UserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("加载认证信息失败: %w", err)
|
||||
}
|
||||
record, err := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(ctx, cmd.UserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找企业信息提交记录失败: %w", err)
|
||||
}
|
||||
if record == nil {
|
||||
return fmt.Errorf("未找到该用户的企业信息提交记录")
|
||||
}
|
||||
switch cmd.TargetStatus {
|
||||
case string(enums.StatusInfoSubmitted):
|
||||
// 审核通过:与 AdminApproveSubmitRecord 一致,推状态并生成企业认证链接
|
||||
switch cert.Status {
|
||||
case enums.StatusInfoSubmitted, enums.StatusEnterpriseVerified, enums.StatusContractApplied,
|
||||
enums.StatusContractSigned, enums.StatusCompleted, enums.StatusContractRejected, enums.StatusContractExpired:
|
||||
return nil
|
||||
}
|
||||
if cert.Status != enums.StatusInfoPendingReview {
|
||||
return fmt.Errorf("认证状态不是待审核,当前: %s", enums.GetStatusName(cert.Status))
|
||||
}
|
||||
enterpriseInfo := &certification_value_objects.EnterpriseInfo{
|
||||
CompanyName: record.CompanyName, UnifiedSocialCode: record.UnifiedSocialCode,
|
||||
LegalPersonName: record.LegalPersonName, LegalPersonID: record.LegalPersonID,
|
||||
LegalPersonPhone: record.LegalPersonPhone, EnterpriseAddress: record.EnterpriseAddress,
|
||||
}
|
||||
authReq := &esign.EnterpriseAuthRequest{
|
||||
CompanyName: enterpriseInfo.CompanyName, UnifiedSocialCode: enterpriseInfo.UnifiedSocialCode,
|
||||
LegalPersonName: enterpriseInfo.LegalPersonName, LegalPersonID: enterpriseInfo.LegalPersonID,
|
||||
TransactorName: enterpriseInfo.LegalPersonName, TransactorMobile: enterpriseInfo.LegalPersonPhone, TransactorID: enterpriseInfo.LegalPersonID,
|
||||
}
|
||||
authURL, alreadyVerified, err := s.generateEnterpriseAuthOrDetectVerified(ctx, authReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("生成企业认证链接失败: %w", err)
|
||||
}
|
||||
if alreadyVerified {
|
||||
if err := cert.ApproveEnterpriseInfoReview("", "", cmd.AdminID); err != nil {
|
||||
return fmt.Errorf("更新认证状态失败: %w", err)
|
||||
}
|
||||
if err := s.completeEnterpriseVerification(ctx, cert, cert.UserID, record.CompanyName, record.LegalPersonName); err != nil {
|
||||
return err
|
||||
}
|
||||
record.MarkManualApproved(cmd.AdminID, cmd.Remark)
|
||||
if err := s.enterpriseInfoSubmitRecordService.Save(ctx, record); err != nil {
|
||||
return fmt.Errorf("保存企业信息提交记录失败: %w", err)
|
||||
}
|
||||
s.logger.Info("管理端变更认证状态为通过", zap.String("user_id", cmd.UserID), zap.String("admin_id", cmd.AdminID))
|
||||
return nil
|
||||
}
|
||||
if err := cert.ApproveEnterpriseInfoReview(authURL.AuthShortURL, authURL.AuthFlowID, cmd.AdminID); err != nil {
|
||||
return fmt.Errorf("更新认证状态失败: %w", err)
|
||||
}
|
||||
if err := s.aggregateService.SaveCertification(ctx, cert); err != nil {
|
||||
return fmt.Errorf("保存认证信息失败: %w", err)
|
||||
}
|
||||
record.MarkManualApproved(cmd.AdminID, cmd.Remark)
|
||||
if err := s.enterpriseInfoSubmitRecordService.Save(ctx, record); err != nil {
|
||||
return fmt.Errorf("保存企业信息提交记录失败: %w", err)
|
||||
}
|
||||
s.logger.Info("管理端变更认证状态为通过", zap.String("user_id", cmd.UserID), zap.String("admin_id", cmd.AdminID))
|
||||
return nil
|
||||
case string(enums.StatusInfoRejected):
|
||||
// 审核拒绝
|
||||
if cert.Status == enums.StatusInfoRejected || cert.Status == enums.StatusEnterpriseVerified ||
|
||||
cert.Status == enums.StatusContractApplied || cert.Status == enums.StatusContractSigned || cert.Status == enums.StatusCompleted {
|
||||
return nil
|
||||
}
|
||||
if cert.Status != enums.StatusInfoPendingReview {
|
||||
return fmt.Errorf("认证状态不是待审核,当前: %s", enums.GetStatusName(cert.Status))
|
||||
}
|
||||
if err := cert.RejectEnterpriseInfoReview(cmd.AdminID, cmd.Remark); err != nil {
|
||||
return fmt.Errorf("更新认证状态失败: %w", err)
|
||||
}
|
||||
if err := s.aggregateService.SaveCertification(ctx, cert); err != nil {
|
||||
return fmt.Errorf("保存认证信息失败: %w", err)
|
||||
}
|
||||
record.MarkManualRejected(cmd.AdminID, cmd.Remark)
|
||||
if err := s.enterpriseInfoSubmitRecordService.Save(ctx, record); err != nil {
|
||||
return fmt.Errorf("保存企业信息提交记录失败: %w", err)
|
||||
}
|
||||
s.logger.Info("管理端变更认证状态为拒绝", zap.String("user_id", cmd.UserID), zap.String("admin_id", cmd.AdminID))
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("不支持的目标状态: %s", cmd.TargetStatus)
|
||||
}
|
||||
}
|
||||
|
||||
// ================ 辅助方法 ================
|
||||
|
||||
// convertToResponse 转换实体为响应DTO
|
||||
@@ -741,6 +1208,66 @@ func (s *CertificationApplicationServiceImpl) convertToResponse(cert *entities.C
|
||||
return response
|
||||
}
|
||||
|
||||
func (s *CertificationApplicationServiceImpl) generateEnterpriseAuthOrDetectVerified(
|
||||
ctx context.Context,
|
||||
req *esign.EnterpriseAuthRequest,
|
||||
) (*esign.EnterpriseAuthResult, bool, error) {
|
||||
s.logger.Info("企业认证链接生成-步骤1-开始调用三方创建认证链接",
|
||||
zap.String("company_name", req.CompanyName),
|
||||
zap.String("unified_social_code", req.UnifiedSocialCode))
|
||||
authURL, err := s.esignClient.GenerateEnterpriseAuth(req)
|
||||
if err == nil {
|
||||
s.logger.Info("企业认证链接生成-步骤1-创建成功",
|
||||
zap.String("company_name", req.CompanyName),
|
||||
zap.String("auth_flow_id", authURL.AuthFlowID))
|
||||
return authURL, false, nil
|
||||
}
|
||||
if !isEnterpriseAlreadyRealnamedErr(err) {
|
||||
s.logger.Error("企业认证链接生成-步骤1-创建失败且非已实名场景",
|
||||
zap.String("company_name", req.CompanyName),
|
||||
zap.Error(err))
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
s.logger.Warn("企业已实名,跳过生成认证链接并转为自动确认",
|
||||
zap.String("company_name", req.CompanyName),
|
||||
zap.String("unified_social_code", req.UnifiedSocialCode),
|
||||
zap.Error(err))
|
||||
|
||||
identity, identityErr := s.esignClient.QueryOrgIdentityInfo(&esign.QueryOrgIdentityRequest{
|
||||
OrgIDCardNum: req.UnifiedSocialCode,
|
||||
OrgIDCardType: esign.OrgIDCardTypeUSCC,
|
||||
})
|
||||
if identityErr != nil {
|
||||
s.logger.Warn("企业认证链接生成-步骤2-按信用代码查询实名状态失败,回退按企业名查询",
|
||||
zap.String("company_name", req.CompanyName),
|
||||
zap.Error(identityErr))
|
||||
identity, identityErr = s.esignClient.QueryOrgIdentityInfo(&esign.QueryOrgIdentityRequest{
|
||||
OrgName: req.CompanyName,
|
||||
})
|
||||
}
|
||||
if identityErr != nil {
|
||||
return nil, false, fmt.Errorf("企业用户已实名,但查询实名状态失败: %w", identityErr)
|
||||
}
|
||||
s.logger.Info("企业认证链接生成-步骤2-实名状态查询成功",
|
||||
zap.String("company_name", req.CompanyName),
|
||||
zap.Int32("realname_status", identity.Data.RealnameStatus))
|
||||
if identity == nil || identity.Data.RealnameStatus != 1 {
|
||||
return nil, false, err
|
||||
}
|
||||
s.logger.Info("企业认证链接生成-步骤3-确认企业已实名,返回自动确认标记",
|
||||
zap.String("company_name", req.CompanyName))
|
||||
return nil, true, nil
|
||||
}
|
||||
|
||||
func isEnterpriseAlreadyRealnamedErr(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
msg := err.Error()
|
||||
return strings.Contains(msg, "企业用户已实名") || strings.Contains(msg, "已实名")
|
||||
}
|
||||
|
||||
// validateApplyContractCommand 验证申请合同命令
|
||||
func (s *CertificationApplicationServiceImpl) validateApplyContractCommand(cmd *commands.ApplyContractCommand) error {
|
||||
if cmd.UserID == "" {
|
||||
@@ -796,6 +1323,9 @@ func (s *CertificationApplicationServiceImpl) completeEnterpriseVerification(
|
||||
companyName string,
|
||||
legalPersonName string,
|
||||
) error {
|
||||
s.logger.Info("完成企业认证-步骤1-开始状态流转",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("company_name", companyName))
|
||||
// 完成企业认证
|
||||
err := cert.CompleteEnterpriseVerification()
|
||||
if err != nil {
|
||||
@@ -809,6 +1339,9 @@ func (s *CertificationApplicationServiceImpl) completeEnterpriseVerification(
|
||||
s.logger.Error("查找企业信息失败", zap.Error(err))
|
||||
return fmt.Errorf("查找企业信息失败: %w", err)
|
||||
}
|
||||
s.logger.Info("完成企业认证-步骤2-获取提交记录成功",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("record_id", record.ID))
|
||||
|
||||
err = s.userAggregateService.CreateEnterpriseInfo(
|
||||
ctx,
|
||||
@@ -832,6 +1365,7 @@ func (s *CertificationApplicationServiceImpl) completeEnterpriseVerification(
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.logger.Info("完成企业认证-步骤3-合同文件生成并写入认证成功", zap.String("user_id", userID))
|
||||
|
||||
// 保存认证信息
|
||||
err = s.aggregateService.SaveCertification(ctx, cert)
|
||||
@@ -839,6 +1373,7 @@ func (s *CertificationApplicationServiceImpl) completeEnterpriseVerification(
|
||||
s.logger.Error("保存认证信息失败", zap.Error(err))
|
||||
return fmt.Errorf("保存认证信息失败: %w", err)
|
||||
}
|
||||
s.logger.Info("完成企业认证-步骤4-认证信息保存成功", zap.String("user_id", userID))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -854,6 +1389,9 @@ func (s *CertificationApplicationServiceImpl) generateAndAddContractFile(
|
||||
legalPersonPhone string,
|
||||
legalPersonID string,
|
||||
) error {
|
||||
s.logger.Info("合同生成-步骤1-开始填充合同模板",
|
||||
zap.String("user_id", cert.UserID),
|
||||
zap.String("company_name", companyName))
|
||||
fileComponent := map[string]string{
|
||||
"YFCompanyName": companyName,
|
||||
"YFCompanyName2": companyName,
|
||||
@@ -872,11 +1410,17 @@ func (s *CertificationApplicationServiceImpl) generateAndAddContractFile(
|
||||
s.logger.Error("生成合同失败", zap.Error(err))
|
||||
return fmt.Errorf("生成合同失败: %s", err.Error())
|
||||
}
|
||||
s.logger.Info("合同生成-步骤1-模板填充成功",
|
||||
zap.String("user_id", cert.UserID),
|
||||
zap.String("file_id", fillTemplateResp.FileID))
|
||||
err = cert.AddContractFileID(fillTemplateResp.FileID, fillTemplateResp.FileDownloadUrl)
|
||||
if err != nil {
|
||||
s.logger.Error("加入合同文件ID链接失败", zap.Error(err))
|
||||
return fmt.Errorf("加入合同文件ID链接失败: %s", err.Error())
|
||||
}
|
||||
s.logger.Info("合同生成-步骤2-合同文件写入认证实体成功",
|
||||
zap.String("user_id", cert.UserID),
|
||||
zap.String("file_id", fillTemplateResp.FileID))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1071,7 +1615,7 @@ func (s *CertificationApplicationServiceImpl) AddStatusMetadata(ctx context.Cont
|
||||
metadata := make(map[string]interface{})
|
||||
metadata = cert.GetDataByStatus()
|
||||
switch cert.Status {
|
||||
case enums.StatusPending, enums.StatusInfoSubmitted, enums.StatusEnterpriseVerified:
|
||||
case enums.StatusPending, enums.StatusInfoPendingReview, enums.StatusInfoRejected, enums.StatusInfoSubmitted, enums.StatusEnterpriseVerified:
|
||||
record, err := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(ctx, cert.UserID)
|
||||
if err == nil && record != nil {
|
||||
enterpriseInfo := map[string]interface{}{
|
||||
@@ -1081,6 +1625,7 @@ func (s *CertificationApplicationServiceImpl) AddStatusMetadata(ctx context.Cont
|
||||
"enterprise_address": record.EnterpriseAddress,
|
||||
"legal_person_phone": record.LegalPersonPhone,
|
||||
"legal_person_id": record.LegalPersonID,
|
||||
"submit_at": record.SubmitAt.Format(time.RFC3339),
|
||||
}
|
||||
metadata["enterprise_info"] = enterpriseInfo
|
||||
}
|
||||
@@ -1097,8 +1642,24 @@ func (s *CertificationApplicationServiceImpl) AddStatusMetadata(ctx context.Cont
|
||||
|
||||
// completeUserActivationWithoutContract 创建钱包、API用户并在用户域标记完成认证(不依赖合同信息)
|
||||
func (s *CertificationApplicationServiceImpl) completeUserActivationWithoutContract(ctx context.Context, cert *entities.Certification) error {
|
||||
// 创建钱包
|
||||
if _, err := s.walletAggregateService.CreateWallet(ctx, cert.UserID); err != nil {
|
||||
// 创建钱包:子账号认证通过后不赠送初始余额(初始额度为0)
|
||||
isSubordinate := false
|
||||
if s.subordinateRepo != nil {
|
||||
if ok, err := s.subordinateRepo.IsUserSubordinate(ctx, cert.UserID); err != nil {
|
||||
s.logger.Warn("检查子账号关系失败,按普通账号处理", zap.String("user_id", cert.UserID), zap.Error(err))
|
||||
} else {
|
||||
isSubordinate = ok
|
||||
}
|
||||
}
|
||||
|
||||
if isSubordinate {
|
||||
if _, err := s.walletRepo.GetByUserID(ctx, cert.UserID); err != nil {
|
||||
zeroWallet := finance_entities.NewWallet(cert.UserID, decimal.Zero)
|
||||
if _, createErr := s.walletRepo.Create(ctx, *zeroWallet); createErr != nil {
|
||||
s.logger.Error("创建子账号钱包失败", zap.String("user_id", cert.UserID), zap.Error(createErr))
|
||||
}
|
||||
}
|
||||
} else if _, err := s.walletAggregateService.CreateWallet(ctx, cert.UserID); err != nil {
|
||||
s.logger.Error("创建钱包失败", zap.String("user_id", cert.UserID), zap.Error(err))
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,14 @@ type ForceTransitionStatusCommand struct {
|
||||
Force bool `json:"force,omitempty"` // 是否强制执行,跳过业务规则验证
|
||||
}
|
||||
|
||||
// AdminTransitionCertificationStatusCommand 管理端变更认证状态(以状态机为准,用于审核通过/拒绝等)
|
||||
type AdminTransitionCertificationStatusCommand struct {
|
||||
AdminID string `json:"-"`
|
||||
UserID string `json:"user_id" validate:"required"`
|
||||
TargetStatus string `json:"target_status" validate:"required,oneof=info_submitted info_rejected"` // 审核通过 -> info_submitted;审核拒绝 -> info_rejected
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// SubmitEnterpriseInfoCommand 提交企业信息命令
|
||||
type SubmitEnterpriseInfoCommand struct {
|
||||
UserID string `json:"-" comment:"用户唯一标识,从JWT token获取,不在JSON中暴露"`
|
||||
@@ -104,4 +112,19 @@ type SubmitEnterpriseInfoCommand struct {
|
||||
LegalPersonPhone string `json:"legal_person_phone" binding:"required,phone" comment:"法定代表人手机号,11位,如:13800138000"`
|
||||
EnterpriseAddress string `json:"enterprise_address" binding:"required,enterprise_address" comment:"企业地址,如:北京市海淀区"`
|
||||
VerificationCode string `json:"verification_code" binding:"required,len=6" comment:"验证码"`
|
||||
|
||||
// 营业执照图片 URL(单张)
|
||||
BusinessLicenseImageURL string `json:"business_license_image_url" binding:"omitempty,url" comment:"营业执照图片URL"`
|
||||
// 办公场地图片 URL 列表(前端传 string 数组)
|
||||
OfficePlaceImageURLs []string `json:"office_place_image_urls" binding:"omitempty,dive,url" comment:"办公场地图片URL列表"`
|
||||
|
||||
// 授权代表信息(与前端 authorized_rep_* 及表字段一致)
|
||||
AuthorizedRepName string `json:"authorized_rep_name" binding:"omitempty,min=2,max=20" comment:"授权代表姓名"`
|
||||
AuthorizedRepID string `json:"authorized_rep_id" binding:"omitempty,id_card" comment:"授权代表身份证号"`
|
||||
AuthorizedRepPhone string `json:"authorized_rep_phone" binding:"omitempty,phone" comment:"授权代表手机号"`
|
||||
AuthorizedRepIDImageURLs []string `json:"authorized_rep_id_image_urls" binding:"omitempty,dive,url" comment:"授权代表身份证正反面图片URL"`
|
||||
|
||||
// 应用场景
|
||||
APIUsage string `json:"api_usage" binding:"omitempty,min=5,max=500" comment:"接口用途及业务场景说明"`
|
||||
ScenarioAttachmentURLs []string `json:"scenario_attachment_urls" binding:"omitempty,dive,url" comment:"场景附件图片URL列表"`
|
||||
}
|
||||
|
||||
@@ -192,3 +192,13 @@ func (q *GetSystemMonitoringQuery) ShouldIncludeMetric(metric string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AdminListSubmitRecordsQuery 管理端企业信息提交记录列表查询(以状态机 certification_status 为准,不做审核状态筛选)
|
||||
type AdminListSubmitRecordsQuery struct {
|
||||
Page int `json:"page" form:"page"`
|
||||
PageSize int `json:"page_size" form:"page_size"`
|
||||
CertificationStatus string `json:"certification_status" form:"certification_status"` // 按认证状态筛选,如 info_pending_review / info_submitted / info_rejected,空为全部
|
||||
CompanyName string `json:"company_name" form:"company_name"` // 企业名称(模糊搜索)
|
||||
LegalPersonPhone string `json:"legal_person_phone" form:"legal_person_phone"` // 法人手机号
|
||||
LegalPersonName string `json:"legal_person_name" form:"legal_person_name"` // 法人姓名(模糊搜索)
|
||||
}
|
||||
|
||||
@@ -53,13 +53,13 @@ type CertificationResponse struct {
|
||||
// ConfirmAuthResponse 确认认证状态响应
|
||||
type ConfirmAuthResponse struct {
|
||||
Status enums.CertificationStatus `json:"status"`
|
||||
Reason string `json:"reason"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// ConfirmSignResponse 确认签署状态响应
|
||||
type ConfirmSignResponse struct {
|
||||
Status enums.CertificationStatus `json:"status"`
|
||||
Reason string `json:"reason"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// CertificationListResponse 认证列表响应
|
||||
@@ -81,7 +81,6 @@ type ContractSignUrlResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
|
||||
// SystemMonitoringResponse 系统监控响应
|
||||
type SystemMonitoringResponse struct {
|
||||
TimeRange string `json:"time_range"`
|
||||
@@ -111,6 +110,55 @@ type SystemHealthStatus struct {
|
||||
Details map[string]interface{} `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// AdminSubmitRecordItem 管理端提交记录列表项
|
||||
type AdminSubmitRecordItem struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
CompanyName string `json:"company_name"`
|
||||
UnifiedSocialCode string `json:"unified_social_code"`
|
||||
LegalPersonName string `json:"legal_person_name"`
|
||||
SubmitAt time.Time `json:"submit_at"`
|
||||
Status string `json:"status"`
|
||||
CertificationStatus string `json:"certification_status,omitempty"` // 以状态机为准:info_pending_review/info_submitted/info_rejected 等
|
||||
}
|
||||
|
||||
// AdminSubmitRecordDetail 管理端提交记录详情(含完整信息与图片 URL)
|
||||
type AdminSubmitRecordDetail struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
CompanyName string `json:"company_name"`
|
||||
UnifiedSocialCode string `json:"unified_social_code"`
|
||||
LegalPersonName string `json:"legal_person_name"`
|
||||
LegalPersonID string `json:"legal_person_id"`
|
||||
LegalPersonPhone string `json:"legal_person_phone"`
|
||||
EnterpriseAddress string `json:"enterprise_address"`
|
||||
AuthorizedRepName string `json:"authorized_rep_name"`
|
||||
AuthorizedRepID string `json:"authorized_rep_id"`
|
||||
AuthorizedRepPhone string `json:"authorized_rep_phone"`
|
||||
AuthorizedRepIDImageURLs string `json:"authorized_rep_id_image_urls"` // JSON 字符串或解析后数组
|
||||
BusinessLicenseImageURL string `json:"business_license_image_url"`
|
||||
OfficePlaceImageURLs string `json:"office_place_image_urls"` // JSON 数组字符串
|
||||
APIUsage string `json:"api_usage"`
|
||||
ScenarioAttachmentURLs string `json:"scenario_attachment_urls"`
|
||||
Status string `json:"status"`
|
||||
SubmitAt time.Time `json:"submit_at"`
|
||||
VerifiedAt *time.Time `json:"verified_at,omitempty"`
|
||||
FailedAt *time.Time `json:"failed_at,omitempty"`
|
||||
FailureReason string `json:"failure_reason,omitempty"`
|
||||
CertificationStatus string `json:"certification_status,omitempty"` // 以状态机为准
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// AdminSubmitRecordsListResponse 管理端提交记录列表响应
|
||||
type AdminSubmitRecordsListResponse struct {
|
||||
Items []*AdminSubmitRecordItem `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
// ================ 响应构建辅助方法 ================
|
||||
|
||||
// NewCertificationListResponse 创建认证列表响应
|
||||
@@ -146,7 +194,6 @@ func NewContractSignUrlResponse(certificationID, signURL, contractURL, nextActio
|
||||
return response
|
||||
}
|
||||
|
||||
|
||||
// NewSystemAlert 创建系统警告
|
||||
func NewSystemAlert(level, alertType, message, metric string, value, threshold interface{}) *SystemAlert {
|
||||
return &SystemAlert{
|
||||
@@ -161,7 +208,6 @@ func NewSystemAlert(level, alertType, message, metric string, value, threshold i
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// IsHealthy 检查系统是否健康
|
||||
func (r *SystemMonitoringResponse) IsHealthy() bool {
|
||||
return r.SystemHealth.Overall == "healthy"
|
||||
|
||||
@@ -1032,7 +1032,8 @@ func (s *ProductApplicationServiceImpl) getDTOMap() map[string]interface{} {
|
||||
"YYSY6F2E": &dto.YYSY6F2EReq{},
|
||||
"YYSY09CD": &dto.YYSY09CDReq{},
|
||||
"IVYZ0B03": &dto.IVYZ0B03Req{},
|
||||
"YYSYBE08": &dto.YYSYBE08Req{},
|
||||
"YYSYBE08": &dto.YYSYBE08Req{},
|
||||
"YYSYBE08TEST": &dto.YYSYBE08Req{},
|
||||
"YYSYD50F": &dto.YYSYD50FReq{},
|
||||
"YYSYF7DB": &dto.YYSYF7DBReq{},
|
||||
"IVYZ9A2B": &dto.IVYZ9A2BReq{},
|
||||
@@ -1069,6 +1070,7 @@ func (s *ProductApplicationServiceImpl) getDTOMap() map[string]interface{} {
|
||||
"DWBG8B4D": &dto.DWBG8B4DReq{},
|
||||
"FLXG8B4D": &dto.FLXG8B4DReq{},
|
||||
"IVYZ81NC": &dto.IVYZ81NCReq{},
|
||||
"IVYZ2MN6": &dto.IVYZ2MN6Req{},
|
||||
"IVYZ7F3A": &dto.IVYZ7F3AReq{},
|
||||
"IVYZ3P9M": &dto.IVYZ3P9MReq{},
|
||||
"IVYZ3A7F": &dto.IVYZ3A7FReq{},
|
||||
|
||||
16
internal/application/product/self_subscribe_policy.go
Normal file
16
internal/application/product/self_subscribe_policy.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package product
|
||||
|
||||
import "context"
|
||||
|
||||
// SelfSubscribePolicy 是否允许用户在控制台自助发起「订阅产品」
|
||||
type SelfSubscribePolicy interface {
|
||||
Allow(ctx context.Context, userID string) (allowed bool, message string, err error)
|
||||
}
|
||||
|
||||
// DefaultAllowSelfSubscribe 未装配下属模块时:恒允许
|
||||
type DefaultAllowSelfSubscribe struct{}
|
||||
|
||||
// Allow 恒允许
|
||||
func (DefaultAllowSelfSubscribe) Allow(_ context.Context, _ string) (bool, string, error) {
|
||||
return true, "", nil
|
||||
}
|
||||
@@ -23,6 +23,7 @@ type SubscriptionApplicationServiceImpl struct {
|
||||
productSubscriptionService *product_service.ProductSubscriptionService
|
||||
userRepo user_repositories.UserRepository
|
||||
apiCallRepository domain_api_repo.ApiCallRepository
|
||||
selfSubscribePolicy SelfSubscribePolicy
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
@@ -31,12 +32,17 @@ func NewSubscriptionApplicationService(
|
||||
productSubscriptionService *product_service.ProductSubscriptionService,
|
||||
userRepo user_repositories.UserRepository,
|
||||
apiCallRepository domain_api_repo.ApiCallRepository,
|
||||
selfSubscribePolicy SelfSubscribePolicy,
|
||||
logger *zap.Logger,
|
||||
) SubscriptionApplicationService {
|
||||
if selfSubscribePolicy == nil {
|
||||
selfSubscribePolicy = DefaultAllowSelfSubscribe{}
|
||||
}
|
||||
return &SubscriptionApplicationServiceImpl{
|
||||
productSubscriptionService: productSubscriptionService,
|
||||
userRepo: userRepo,
|
||||
apiCallRepository: apiCallRepository,
|
||||
selfSubscribePolicy: selfSubscribePolicy,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
@@ -157,7 +163,17 @@ func (s *SubscriptionApplicationServiceImpl) BatchUpdateSubscriptionPrices(ctx c
|
||||
// CreateSubscription 创建订阅
|
||||
// 业务流程:1. 创建订阅
|
||||
func (s *SubscriptionApplicationServiceImpl) CreateSubscription(ctx context.Context, cmd *commands.CreateSubscriptionCommand) error {
|
||||
_, err := s.productSubscriptionService.CreateSubscription(ctx, cmd.UserID, cmd.ProductID)
|
||||
allow, msg, err := s.selfSubscribePolicy.Allow(ctx, cmd.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !allow {
|
||||
if msg == "" {
|
||||
msg = "当前账号不允许自助订阅"
|
||||
}
|
||||
return fmt.Errorf("%s", msg)
|
||||
}
|
||||
_, err = s.productSubscriptionService.CreateSubscription(ctx, cmd.UserID, cmd.ProductID)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package commands
|
||||
|
||||
// SubPortalRegisterCommand 子站注册(邀请码必填)
|
||||
type SubPortalRegisterCommand struct {
|
||||
Phone string `json:"phone" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
ConfirmPassword string `json:"confirm_password" binding:"required"`
|
||||
Code string `json:"code" binding:"required"`
|
||||
InviteToken string `json:"invite_token" binding:"required"`
|
||||
}
|
||||
|
||||
// CreateInvitationCommand 主账号创建邀请
|
||||
type CreateInvitationCommand struct {
|
||||
ParentUserID string
|
||||
// ExpiresInHours 可选,0 用默认 168 小时
|
||||
ExpiresInHours int `json:"expires_in_hours"`
|
||||
}
|
||||
|
||||
// AllocateToChildCommand 主账号向下属划余额
|
||||
type AllocateToChildCommand struct {
|
||||
ParentUserID string
|
||||
ChildUserID string `json:"child_user_id" binding:"required"`
|
||||
Amount string `json:"amount" binding:"required"`
|
||||
VerifyCode string `json:"verify_code" binding:"required,len=6"`
|
||||
}
|
||||
|
||||
// AssignChildSubscriptionCommand 为下属代配订阅
|
||||
type AssignChildSubscriptionCommand struct {
|
||||
ParentUserID string
|
||||
ChildUserID string `json:"child_user_id" binding:"required"`
|
||||
ProductID string `json:"product_id" binding:"required"`
|
||||
Price string `json:"price" binding:"required"`
|
||||
UIComponentPrice string `json:"ui_component_price"`
|
||||
}
|
||||
|
||||
// ListChildAllocationsCommand 下属划拨记录查询
|
||||
type ListChildAllocationsCommand struct {
|
||||
ParentUserID string
|
||||
ChildUserID string `json:"child_user_id" form:"child_user_id" binding:"required"`
|
||||
Page int `json:"page" form:"page"`
|
||||
PageSize int `json:"page_size" form:"page_size"`
|
||||
}
|
||||
|
||||
// ListChildSubscriptionsCommand 下属订阅列表查询
|
||||
type ListChildSubscriptionsCommand struct {
|
||||
ParentUserID string
|
||||
ChildUserID string `json:"child_user_id" form:"child_user_id" binding:"required"`
|
||||
}
|
||||
|
||||
// RemoveChildSubscriptionCommand 删除下属订阅
|
||||
type RemoveChildSubscriptionCommand struct {
|
||||
ParentUserID string
|
||||
ChildUserID string `json:"child_user_id" binding:"required"`
|
||||
SubscriptionID string `json:"subscription_id" binding:"required"`
|
||||
}
|
||||
|
||||
// PurchaseChildQuotaCommand 主账号为子账号购买调用额度
|
||||
type PurchaseChildQuotaCommand struct {
|
||||
ParentUserID string
|
||||
ChildUserID string `json:"child_user_id" binding:"required"`
|
||||
ProductID string `json:"product_id" binding:"required"`
|
||||
CallCount int64 `json:"call_count" binding:"required,min=1"`
|
||||
VerifyCode string `json:"verify_code" binding:"required,len=6"`
|
||||
}
|
||||
|
||||
// ListChildQuotaPurchasesCommand 下属额度购买记录查询
|
||||
type ListChildQuotaPurchasesCommand struct {
|
||||
ParentUserID string
|
||||
ChildUserID string `json:"child_user_id" form:"child_user_id" binding:"required"`
|
||||
Page int `json:"page" form:"page"`
|
||||
PageSize int `json:"page_size" form:"page_size"`
|
||||
}
|
||||
|
||||
// ListChildQuotaAccountsCommand 下属额度账户查询
|
||||
type ListChildQuotaAccountsCommand struct {
|
||||
ParentUserID string
|
||||
ChildUserID string `json:"child_user_id" form:"child_user_id" binding:"required"`
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package responses
|
||||
|
||||
import "time"
|
||||
|
||||
// CreateInvitationResponse 创建邀请
|
||||
type CreateInvitationResponse struct {
|
||||
InviteToken string `json:"invite_token" description:"仅返回一次,请转达被邀请人"`
|
||||
InviteURL string `json:"invite_url" description:"子站注册完整链接"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
InvitationID string `json:"invitation_id"`
|
||||
}
|
||||
|
||||
// SubordinateListItem 下属一条
|
||||
type SubordinateListItem struct {
|
||||
ChildUserID string `json:"child_user_id"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
LinkID string `json:"link_id"`
|
||||
RegisteredAt time.Time `json:"registered_at"`
|
||||
CompanyName string `json:"company_name"`
|
||||
IsCertified bool `json:"is_certified"`
|
||||
Balance string `json:"balance"`
|
||||
}
|
||||
|
||||
// SubordinateListResponse 列表
|
||||
type SubordinateListResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
Items []SubordinateListItem `json:"items"`
|
||||
}
|
||||
|
||||
// SubPortalRegisterResponse 子站注册
|
||||
type SubPortalRegisterResponse struct {
|
||||
ID string `json:"id"`
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
|
||||
// ChildAllocationItem 下属划拨记录
|
||||
type ChildAllocationItem struct {
|
||||
ID string `json:"id"`
|
||||
Amount string `json:"amount"`
|
||||
BusinessRef string `json:"business_ref"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ChildAllocationListResponse 下属划拨记录列表
|
||||
type ChildAllocationListResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
Items []ChildAllocationItem `json:"items"`
|
||||
}
|
||||
|
||||
// ChildSubscriptionItem 下属订阅项
|
||||
type ChildSubscriptionItem struct {
|
||||
ID string `json:"id"`
|
||||
ProductID string `json:"product_id"`
|
||||
Price string `json:"price"`
|
||||
UIComponentPrice string `json:"ui_component_price"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ChildQuotaPurchaseItem 下属额度购买记录
|
||||
type ChildQuotaPurchaseItem struct {
|
||||
ID string `json:"id"`
|
||||
ProductID string `json:"product_id"`
|
||||
CallCount int64 `json:"call_count"`
|
||||
UnitPrice string `json:"unit_price"`
|
||||
TotalAmount string `json:"total_amount"`
|
||||
BusinessRef string `json:"business_ref"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ChildQuotaPurchaseListResponse 下属额度购买记录列表
|
||||
type ChildQuotaPurchaseListResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
Items []ChildQuotaPurchaseItem `json:"items"`
|
||||
}
|
||||
|
||||
// ChildQuotaAccountItem 下属产品额度账户
|
||||
type ChildQuotaAccountItem struct {
|
||||
ProductID string `json:"product_id"`
|
||||
TotalQuota int64 `json:"total_quota"`
|
||||
UsedQuota int64 `json:"used_quota"`
|
||||
AvailableQuota int64 `json:"available_quota"`
|
||||
}
|
||||
35
internal/application/subordinate/invite_token.go
Normal file
35
internal/application/subordinate/invite_token.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package subordinate
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
const (
|
||||
// 邀请码固定 6 位,字符集为大写字母+数字
|
||||
inviteTokenLength = 6
|
||||
inviteTokenCharset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
)
|
||||
|
||||
// HashInviteToken 邀请码 SHA256 十六进制
|
||||
func HashInviteToken(raw string) string {
|
||||
sum := sha256.Sum256([]byte(raw))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// GenerateInviteToken 生成随机邀请明文与存储用哈希
|
||||
func GenerateInviteToken() (raw string, hash string, err error) {
|
||||
token := make([]byte, inviteTokenLength)
|
||||
charsetSize := big.NewInt(int64(len(inviteTokenCharset)))
|
||||
for i := range token {
|
||||
n, e := rand.Int(rand.Reader, charsetSize)
|
||||
if e != nil {
|
||||
return "", "", e
|
||||
}
|
||||
token[i] = inviteTokenCharset[n.Int64()]
|
||||
}
|
||||
raw = string(token)
|
||||
return raw, HashInviteToken(raw), nil
|
||||
}
|
||||
26
internal/application/subordinate/invite_token_test.go
Normal file
26
internal/application/subordinate/invite_token_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package subordinate
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGenerateInviteTokenFormat(t *testing.T) {
|
||||
raw, hash, err := GenerateInviteToken()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateInviteToken error: %v", err)
|
||||
}
|
||||
|
||||
if len(raw) != inviteTokenLength {
|
||||
t.Fatalf("unexpected token length: got %d, want %d", len(raw), inviteTokenLength)
|
||||
}
|
||||
|
||||
for _, ch := range raw {
|
||||
isUpper := ch >= 'A' && ch <= 'Z'
|
||||
isDigit := ch >= '0' && ch <= '9'
|
||||
if !isUpper && !isDigit {
|
||||
t.Fatalf("token contains invalid char: %q", ch)
|
||||
}
|
||||
}
|
||||
|
||||
if hash != HashInviteToken(raw) {
|
||||
t.Fatalf("hash mismatch for token")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package subordinate
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tyapi-server/internal/application/product"
|
||||
"tyapi-server/internal/domains/subordinate/repositories"
|
||||
)
|
||||
|
||||
// BlockSelfSubscribeForSubordinate 子账号禁止自助订
|
||||
type BlockSelfSubscribeForSubordinate struct {
|
||||
repo repositories.SubordinateRepository
|
||||
}
|
||||
|
||||
// NewBlockSelfSubscribeForSubordinate 构造
|
||||
func NewBlockSelfSubscribeForSubordinate(repo repositories.SubordinateRepository) product.SelfSubscribePolicy {
|
||||
return &BlockSelfSubscribeForSubordinate{repo: repo}
|
||||
}
|
||||
|
||||
// Allow 若为主账号的下属则拒绝
|
||||
func (p *BlockSelfSubscribeForSubordinate) Allow(ctx context.Context, userID string) (bool, string, error) {
|
||||
ok, err := p.repo.IsUserSubordinate(ctx, userID)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
if ok {
|
||||
return false, "子账号需由主账号配置订阅", nil
|
||||
}
|
||||
return true, "", nil
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package subordinate
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tyapi-server/internal/application/subordinate/dto/commands"
|
||||
"tyapi-server/internal/application/subordinate/dto/responses"
|
||||
)
|
||||
|
||||
// SubordinateApplicationService 下属账号:邀请/注册/划款/代配
|
||||
type SubordinateApplicationService interface {
|
||||
RegisterSubPortal(ctx context.Context, cmd *commands.SubPortalRegisterCommand) (*responses.SubPortalRegisterResponse, error)
|
||||
CreateInvitation(ctx context.Context, cmd *commands.CreateInvitationCommand) (*responses.CreateInvitationResponse, error)
|
||||
ListMySubordinates(ctx context.Context, parentUserID string, page, pageSize int) (*responses.SubordinateListResponse, error)
|
||||
AllocateToChild(ctx context.Context, cmd *commands.AllocateToChildCommand) error
|
||||
ListChildAllocations(ctx context.Context, cmd *commands.ListChildAllocationsCommand) (*responses.ChildAllocationListResponse, error)
|
||||
AssignChildSubscription(ctx context.Context, cmd *commands.AssignChildSubscriptionCommand) error
|
||||
ListChildSubscriptions(ctx context.Context, cmd *commands.ListChildSubscriptionsCommand) ([]responses.ChildSubscriptionItem, error)
|
||||
RemoveChildSubscription(ctx context.Context, cmd *commands.RemoveChildSubscriptionCommand) error
|
||||
PurchaseChildQuota(ctx context.Context, cmd *commands.PurchaseChildQuotaCommand) error
|
||||
ListChildQuotaPurchases(ctx context.Context, cmd *commands.ListChildQuotaPurchasesCommand) (*responses.ChildQuotaPurchaseListResponse, error)
|
||||
ListChildQuotaAccounts(ctx context.Context, cmd *commands.ListChildQuotaAccountsCommand) ([]responses.ChildQuotaAccountItem, error)
|
||||
ListMyQuotaAccounts(ctx context.Context, userID string) ([]responses.ChildQuotaAccountItem, error)
|
||||
}
|
||||
@@ -0,0 +1,608 @@
|
||||
package subordinate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/application/subordinate/dto/commands"
|
||||
"tyapi-server/internal/application/subordinate/dto/responses"
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/domains/finance/repositories"
|
||||
productentities "tyapi-server/internal/domains/product/entities"
|
||||
product_service "tyapi-server/internal/domains/product/services"
|
||||
subentities "tyapi-server/internal/domains/subordinate/entities"
|
||||
subrepositories "tyapi-server/internal/domains/subordinate/repositories"
|
||||
user_entities "tyapi-server/internal/domains/user/entities"
|
||||
user_repositories "tyapi-server/internal/domains/user/repositories"
|
||||
domain_user_services "tyapi-server/internal/domains/user/services"
|
||||
"tyapi-server/internal/shared/database"
|
||||
)
|
||||
|
||||
// SubordinateApplicationServiceImpl 实现
|
||||
type SubordinateApplicationServiceImpl struct {
|
||||
subRepo subrepositories.SubordinateRepository
|
||||
userAgg domain_user_services.UserAggregateService
|
||||
smsService *domain_user_services.SMSCodeService
|
||||
productSub *product_service.ProductSubscriptionService
|
||||
cfg *config.Config
|
||||
txm *database.TransactionManager
|
||||
walletRepo repositories.WalletRepository
|
||||
userRepo user_repositories.UserRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewSubordinateApplicationService 构造
|
||||
func NewSubordinateApplicationService(
|
||||
subRepo subrepositories.SubordinateRepository,
|
||||
userAgg domain_user_services.UserAggregateService,
|
||||
smsService *domain_user_services.SMSCodeService,
|
||||
productSub *product_service.ProductSubscriptionService,
|
||||
cfg *config.Config,
|
||||
txm *database.TransactionManager,
|
||||
walletRepo repositories.WalletRepository,
|
||||
userRepo user_repositories.UserRepository,
|
||||
logger *zap.Logger,
|
||||
) SubordinateApplicationService {
|
||||
return &SubordinateApplicationServiceImpl{
|
||||
subRepo: subRepo,
|
||||
userAgg: userAgg,
|
||||
smsService: smsService,
|
||||
productSub: productSub,
|
||||
cfg: cfg,
|
||||
txm: txm,
|
||||
walletRepo: walletRepo,
|
||||
userRepo: userRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterSubPortal 子站注册
|
||||
func (s *SubordinateApplicationServiceImpl) RegisterSubPortal(ctx context.Context, cmd *commands.SubPortalRegisterCommand) (*responses.SubPortalRegisterResponse, error) {
|
||||
if cmd.Password != cmd.ConfirmPassword {
|
||||
return nil, fmt.Errorf("两次输入的密码不一致")
|
||||
}
|
||||
if err := s.smsService.VerifyCode(ctx, cmd.Phone, cmd.Code, user_entities.SMSSceneRegister); err != nil {
|
||||
return nil, fmt.Errorf("验证码错误或已过期")
|
||||
}
|
||||
|
||||
var resp *responses.SubPortalRegisterResponse
|
||||
err := s.txm.ExecuteInTx(ctx, func(txCtx context.Context) error {
|
||||
inv, err := s.subRepo.FindInvitationByTokenHash(txCtx, HashInviteToken(strings.TrimSpace(cmd.InviteToken)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if inv == nil {
|
||||
return fmt.Errorf("邀请码无效")
|
||||
}
|
||||
if inv.Status != subentities.InvitationStatusPending {
|
||||
return fmt.Errorf("邀请码已使用或已失效")
|
||||
}
|
||||
now := time.Now()
|
||||
if now.After(inv.ExpiresAt) {
|
||||
return fmt.Errorf("邀请码已过期")
|
||||
}
|
||||
|
||||
u, createErr := s.userAgg.CreateUser(txCtx, cmd.Phone, cmd.Password)
|
||||
if createErr != nil {
|
||||
return createErr
|
||||
}
|
||||
|
||||
link := &subentities.UserSubordinateLink{
|
||||
ParentUserID: inv.ParentUserID,
|
||||
ChildUserID: u.ID,
|
||||
InvitationID: &inv.ID,
|
||||
Status: subentities.LinkStatusActive,
|
||||
}
|
||||
if linkErr := s.subRepo.CreateLink(txCtx, link); linkErr != nil {
|
||||
s.logger.Error("创建主从关系失败", zap.Error(linkErr), zap.String("user_id", u.ID))
|
||||
return fmt.Errorf("注册失败,请重试或联系主账号")
|
||||
}
|
||||
|
||||
consumed, consumeErr := s.subRepo.ConsumeInvitation(txCtx, inv.ID, u.ID, now)
|
||||
if consumeErr != nil {
|
||||
s.logger.Error("核销邀请失败", zap.Error(consumeErr), zap.String("user_id", u.ID))
|
||||
return fmt.Errorf("注册失败,请重试或联系主账号")
|
||||
}
|
||||
if !consumed {
|
||||
return fmt.Errorf("邀请码已使用或已失效")
|
||||
}
|
||||
|
||||
resp = &responses.SubPortalRegisterResponse{ID: u.ID, Phone: u.Phone}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// CreateInvitation 主账号发邀请
|
||||
func (s *SubordinateApplicationServiceImpl) CreateInvitation(ctx context.Context, cmd *commands.CreateInvitationCommand) (*responses.CreateInvitationResponse, error) {
|
||||
hours := cmd.ExpiresInHours
|
||||
if hours <= 0 {
|
||||
hours = 24 * 7
|
||||
}
|
||||
raw, hash, err := GenerateInviteToken()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("生成邀请失败")
|
||||
}
|
||||
inv := &subentities.SubordinateInvitation{
|
||||
ParentUserID: cmd.ParentUserID,
|
||||
TokenHash: hash,
|
||||
ExpiresAt: time.Now().Add(time.Duration(hours) * time.Hour),
|
||||
Status: subentities.InvitationStatusPending,
|
||||
}
|
||||
if err := s.subRepo.CreateInvitation(ctx, inv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
base := strings.TrimSpace(os.Getenv("SUB_PORTAL_BASE_URL"))
|
||||
if base == "" {
|
||||
base = s.cfg.App.SubPortalBaseURL
|
||||
}
|
||||
base = strings.TrimRight(base, "/")
|
||||
if base == "" {
|
||||
return nil, fmt.Errorf("子账号域名未配置,请设置 app.sub_portal_base_url 或环境变量 SUB_PORTAL_BASE_URL")
|
||||
}
|
||||
// 与前端同仓路由一致:/sub/auth/register 为子账号专用注册页
|
||||
inviteURL := base + "/sub/auth/register?invite=" + raw
|
||||
return &responses.CreateInvitationResponse{
|
||||
InviteToken: raw,
|
||||
InviteURL: inviteURL,
|
||||
ExpiresAt: inv.ExpiresAt,
|
||||
InvitationID: inv.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListMySubordinates 主账号的下属
|
||||
func (s *SubordinateApplicationServiceImpl) ListMySubordinates(ctx context.Context, parentUserID string, page, pageSize int) (*responses.SubordinateListResponse, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
links, total, err := s.subRepo.ListChildrenByParent(ctx, parentUserID, pageSize, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]responses.SubordinateListItem, 0, len(links))
|
||||
for _, ln := range links {
|
||||
phone := ""
|
||||
companyName := "未认证"
|
||||
isCertified := false
|
||||
registeredAt := ln.CreatedAt
|
||||
balance := "0.00"
|
||||
|
||||
if u, e := s.userRepo.GetByIDWithEnterpriseInfo(ctx, ln.ChildUserID); e == nil {
|
||||
phone = u.Phone
|
||||
isCertified = u.IsCertified
|
||||
registeredAt = u.CreatedAt
|
||||
if u.EnterpriseInfo != nil && strings.TrimSpace(u.EnterpriseInfo.CompanyName) != "" {
|
||||
companyName = strings.TrimSpace(u.EnterpriseInfo.CompanyName)
|
||||
}
|
||||
} else {
|
||||
s.logger.Warn("获取下属用户失败", zap.String("child_id", ln.ChildUserID), zap.Error(e))
|
||||
}
|
||||
if w, e := s.walletRepo.GetByUserID(ctx, ln.ChildUserID); e == nil && w != nil {
|
||||
balance = w.Balance.StringFixed(2)
|
||||
}
|
||||
items = append(items, responses.SubordinateListItem{
|
||||
ChildUserID: ln.ChildUserID,
|
||||
Phone: phone,
|
||||
LinkID: ln.ID,
|
||||
RegisteredAt: registeredAt,
|
||||
CompanyName: companyName,
|
||||
IsCertified: isCertified,
|
||||
Balance: balance,
|
||||
})
|
||||
}
|
||||
return &responses.SubordinateListResponse{Total: total, Items: items}, nil
|
||||
}
|
||||
|
||||
// AllocateToChild 划款
|
||||
func (s *SubordinateApplicationServiceImpl) AllocateToChild(ctx context.Context, cmd *commands.AllocateToChildCommand) error {
|
||||
amount, err := decimal.NewFromString(strings.TrimSpace(cmd.Amount))
|
||||
if err != nil || !amount.GreaterThan(decimal.Zero) {
|
||||
return fmt.Errorf("金额必须大于0")
|
||||
}
|
||||
parentUser, err := s.userRepo.GetByID(ctx, cmd.ParentUserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("主账号信息获取失败")
|
||||
}
|
||||
if err := s.smsService.VerifyCode(ctx, parentUser.Phone, strings.TrimSpace(cmd.VerifyCode), user_entities.SMSSceneLogin); err != nil {
|
||||
return fmt.Errorf("验证码错误或已过期")
|
||||
}
|
||||
lnk, err := s.subRepo.FindLinkByParentAndChild(ctx, cmd.ParentUserID, cmd.ChildUserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if lnk == nil || lnk.Status != subentities.LinkStatusActive {
|
||||
return fmt.Errorf("该用户不是您的有效下属")
|
||||
}
|
||||
bizRef := uuid.New().String()
|
||||
return s.txm.ExecuteInTx(ctx, func(txCtx context.Context) error {
|
||||
ok, err := s.walletRepo.UpdateBalanceByUserID(txCtx, cmd.ParentUserID, amount, "subtract")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("主账号扣款失败,请重试")
|
||||
}
|
||||
ok2, err := s.walletRepo.UpdateBalanceByUserID(txCtx, cmd.ChildUserID, amount, "add")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok2 {
|
||||
return fmt.Errorf("向下属入账失败,请重试")
|
||||
}
|
||||
alloc := &subentities.SubordinateWalletAllocation{
|
||||
FromUserID: cmd.ParentUserID,
|
||||
ToUserID: cmd.ChildUserID,
|
||||
Amount: amount,
|
||||
BusinessRef: bizRef,
|
||||
OperatorUserID: cmd.ParentUserID,
|
||||
}
|
||||
return s.subRepo.CreateWalletAllocation(txCtx, alloc)
|
||||
})
|
||||
}
|
||||
|
||||
// ListChildAllocations 下属划拨记录
|
||||
func (s *SubordinateApplicationServiceImpl) ListChildAllocations(ctx context.Context, cmd *commands.ListChildAllocationsCommand) (*responses.ChildAllocationListResponse, error) {
|
||||
lnk, err := s.subRepo.FindLinkByParentAndChild(ctx, cmd.ParentUserID, cmd.ChildUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if lnk == nil || lnk.Status != subentities.LinkStatusActive {
|
||||
return nil, fmt.Errorf("该用户不是您的有效下属")
|
||||
}
|
||||
page := cmd.Page
|
||||
pageSize := cmd.PageSize
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
rows, total, err := s.subRepo.ListWalletAllocationsByParentAndChild(ctx, cmd.ParentUserID, cmd.ChildUserID, pageSize, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]responses.ChildAllocationItem, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, responses.ChildAllocationItem{
|
||||
ID: row.ID,
|
||||
Amount: row.Amount.StringFixed(2),
|
||||
BusinessRef: row.BusinessRef,
|
||||
CreatedAt: row.CreatedAt,
|
||||
})
|
||||
}
|
||||
return &responses.ChildAllocationListResponse{
|
||||
Total: total,
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AssignChildSubscription 代配订阅
|
||||
func (s *SubordinateApplicationServiceImpl) AssignChildSubscription(ctx context.Context, cmd *commands.AssignChildSubscriptionCommand) error {
|
||||
lnk, err := s.subRepo.FindLinkByParentAndChild(ctx, cmd.ParentUserID, cmd.ChildUserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if lnk == nil || lnk.Status != subentities.LinkStatusActive {
|
||||
return fmt.Errorf("该用户不是您的有效下属")
|
||||
}
|
||||
|
||||
price, err := decimal.NewFromString(strings.TrimSpace(cmd.Price))
|
||||
if err != nil {
|
||||
return fmt.Errorf("价格格式无效")
|
||||
}
|
||||
|
||||
parentSub, err := s.productSub.GetUserSubscribedProduct(ctx, cmd.ParentUserID, cmd.ProductID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if parentSub == nil {
|
||||
return fmt.Errorf("主账号未订阅该产品,无法为下属代配")
|
||||
}
|
||||
if price.LessThan(parentSub.Price) {
|
||||
return fmt.Errorf("下属订阅价不能低于主账号对该产品的订阅价")
|
||||
}
|
||||
|
||||
uip := parentSub.UIComponentPrice
|
||||
if strings.TrimSpace(cmd.UIComponentPrice) != "" {
|
||||
p, err2 := decimal.NewFromString(strings.TrimSpace(cmd.UIComponentPrice))
|
||||
if err2 != nil {
|
||||
return fmt.Errorf("UI组件价格格式无效")
|
||||
}
|
||||
if p.LessThan(parentSub.UIComponentPrice) {
|
||||
return fmt.Errorf("下属 UI 组合价不能低于主账号的 UI 组合价")
|
||||
}
|
||||
uip = p
|
||||
}
|
||||
|
||||
existing, err := s.productSub.GetUserSubscribedProduct(ctx, cmd.ChildUserID, cmd.ProductID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existing == nil {
|
||||
newSub := &productentities.Subscription{
|
||||
UserID: cmd.ChildUserID,
|
||||
ProductID: cmd.ProductID,
|
||||
Price: price,
|
||||
UIComponentPrice: uip,
|
||||
}
|
||||
return s.productSub.SaveSubscription(ctx, newSub)
|
||||
}
|
||||
existing.Price = price
|
||||
existing.UIComponentPrice = uip
|
||||
return s.productSub.SaveSubscription(ctx, existing)
|
||||
}
|
||||
|
||||
// ListChildSubscriptions 下属订阅列表
|
||||
func (s *SubordinateApplicationServiceImpl) ListChildSubscriptions(ctx context.Context, cmd *commands.ListChildSubscriptionsCommand) ([]responses.ChildSubscriptionItem, error) {
|
||||
lnk, err := s.subRepo.FindLinkByParentAndChild(ctx, cmd.ParentUserID, cmd.ChildUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if lnk == nil || lnk.Status != subentities.LinkStatusActive {
|
||||
return nil, fmt.Errorf("该用户不是您的有效下属")
|
||||
}
|
||||
subs, err := s.productSub.GetUserSubscriptions(ctx, cmd.ChildUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]responses.ChildSubscriptionItem, 0, len(subs))
|
||||
for _, sub := range subs {
|
||||
items = append(items, responses.ChildSubscriptionItem{
|
||||
ID: sub.ID,
|
||||
ProductID: sub.ProductID,
|
||||
Price: sub.Price.StringFixed(2),
|
||||
UIComponentPrice: sub.UIComponentPrice.StringFixed(2),
|
||||
CreatedAt: sub.CreatedAt,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// RemoveChildSubscription 删除下属订阅
|
||||
func (s *SubordinateApplicationServiceImpl) RemoveChildSubscription(ctx context.Context, cmd *commands.RemoveChildSubscriptionCommand) error {
|
||||
lnk, err := s.subRepo.FindLinkByParentAndChild(ctx, cmd.ParentUserID, cmd.ChildUserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if lnk == nil || lnk.Status != subentities.LinkStatusActive {
|
||||
return fmt.Errorf("该用户不是您的有效下属")
|
||||
}
|
||||
sub, err := s.productSub.GetSubscriptionByID(ctx, cmd.SubscriptionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("订阅不存在")
|
||||
}
|
||||
if sub.UserID != cmd.ChildUserID {
|
||||
return fmt.Errorf("订阅不属于该下属")
|
||||
}
|
||||
return s.productSub.CancelSubscription(ctx, cmd.SubscriptionID)
|
||||
}
|
||||
|
||||
// PurchaseChildQuota 主账号为子账号购买调用额度(按子账号订阅价结算)
|
||||
func (s *SubordinateApplicationServiceImpl) PurchaseChildQuota(ctx context.Context, cmd *commands.PurchaseChildQuotaCommand) error {
|
||||
if cmd.CallCount <= 0 {
|
||||
return fmt.Errorf("购买次数必须大于0")
|
||||
}
|
||||
|
||||
parentUser, err := s.userRepo.GetByID(ctx, cmd.ParentUserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("主账号信息获取失败")
|
||||
}
|
||||
if err := s.smsService.VerifyCode(ctx, parentUser.Phone, strings.TrimSpace(cmd.VerifyCode), user_entities.SMSSceneLogin); err != nil {
|
||||
return fmt.Errorf("验证码错误或已过期")
|
||||
}
|
||||
|
||||
lnk, err := s.subRepo.FindLinkByParentAndChild(ctx, cmd.ParentUserID, cmd.ChildUserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if lnk == nil || lnk.Status != subentities.LinkStatusActive {
|
||||
return fmt.Errorf("该用户不是您的有效下属")
|
||||
}
|
||||
|
||||
parentSub, err := s.productSub.GetUserSubscribedProduct(ctx, cmd.ParentUserID, cmd.ProductID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if parentSub == nil {
|
||||
return fmt.Errorf("主账号未订阅该产品,无法购买额度")
|
||||
}
|
||||
if !parentSub.Price.GreaterThan(decimal.Zero) {
|
||||
return fmt.Errorf("主账号订阅价格异常,无法购买额度")
|
||||
}
|
||||
|
||||
callCountDec := decimal.NewFromInt(cmd.CallCount)
|
||||
totalAmount := parentSub.Price.Mul(callCountDec)
|
||||
if !totalAmount.GreaterThan(decimal.Zero) {
|
||||
return fmt.Errorf("购买金额必须大于0")
|
||||
}
|
||||
|
||||
bizRef := uuid.New().String()
|
||||
return s.txm.ExecuteInTx(ctx, func(txCtx context.Context) error {
|
||||
// 购买额度前自动确保子账号存在该产品订阅,并统一为主账号订阅价
|
||||
childSub, err := s.productSub.GetUserSubscribedProduct(txCtx, cmd.ChildUserID, cmd.ProductID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if childSub == nil {
|
||||
newSub := &productentities.Subscription{
|
||||
UserID: cmd.ChildUserID,
|
||||
ProductID: cmd.ProductID,
|
||||
Price: parentSub.Price,
|
||||
UIComponentPrice: parentSub.UIComponentPrice,
|
||||
}
|
||||
if err := s.productSub.SaveSubscription(txCtx, newSub); err != nil {
|
||||
return fmt.Errorf("为下属创建订阅失败: %w", err)
|
||||
}
|
||||
} else {
|
||||
childSub.Price = parentSub.Price
|
||||
childSub.UIComponentPrice = parentSub.UIComponentPrice
|
||||
if err := s.productSub.SaveSubscription(txCtx, childSub); err != nil {
|
||||
return fmt.Errorf("更新下属订阅失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
ok, err := s.walletRepo.UpdateBalanceByUserID(txCtx, cmd.ParentUserID, totalAmount, "subtract")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("主账号扣款失败,请重试")
|
||||
}
|
||||
|
||||
account, err := s.subRepo.FindQuotaAccount(txCtx, cmd.ChildUserID, cmd.ProductID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var beforeAvailable int64
|
||||
if account == nil {
|
||||
account = &subentities.UserProductQuotaAccount{
|
||||
UserID: cmd.ChildUserID,
|
||||
ProductID: cmd.ProductID,
|
||||
TotalQuota: cmd.CallCount,
|
||||
UsedQuota: 0,
|
||||
AvailableQuota: cmd.CallCount,
|
||||
}
|
||||
beforeAvailable = 0
|
||||
if err := s.subRepo.CreateQuotaAccount(txCtx, account); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
beforeAvailable = account.AvailableQuota
|
||||
account.TotalQuota += cmd.CallCount
|
||||
account.AvailableQuota += cmd.CallCount
|
||||
if err := s.subRepo.UpdateQuotaAccount(txCtx, account); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
purchase := &subentities.SubordinateQuotaPurchase{
|
||||
ParentUserID: cmd.ParentUserID,
|
||||
ChildUserID: cmd.ChildUserID,
|
||||
ProductID: cmd.ProductID,
|
||||
CallCount: cmd.CallCount,
|
||||
UnitPrice: parentSub.Price,
|
||||
TotalAmount: totalAmount,
|
||||
BusinessRef: bizRef,
|
||||
OperatorUserID: cmd.ParentUserID,
|
||||
}
|
||||
if err := s.subRepo.CreateQuotaPurchase(txCtx, purchase); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ledger := &subentities.UserProductQuotaLedger{
|
||||
UserID: cmd.ChildUserID,
|
||||
ProductID: cmd.ProductID,
|
||||
ChangeType: subentities.QuotaLedgerChangeTypePurchaseForSub,
|
||||
DeltaQuota: cmd.CallCount,
|
||||
BeforeQuota: beforeAvailable,
|
||||
AfterQuota: beforeAvailable + cmd.CallCount,
|
||||
SourceID: purchase.ID,
|
||||
OperatorID: cmd.ParentUserID,
|
||||
Remark: "主账号为子账号购买额度",
|
||||
}
|
||||
return s.subRepo.CreateQuotaLedger(txCtx, ledger)
|
||||
})
|
||||
}
|
||||
|
||||
// ListChildQuotaPurchases 下属额度购买记录
|
||||
func (s *SubordinateApplicationServiceImpl) ListChildQuotaPurchases(ctx context.Context, cmd *commands.ListChildQuotaPurchasesCommand) (*responses.ChildQuotaPurchaseListResponse, error) {
|
||||
lnk, err := s.subRepo.FindLinkByParentAndChild(ctx, cmd.ParentUserID, cmd.ChildUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if lnk == nil || lnk.Status != subentities.LinkStatusActive {
|
||||
return nil, fmt.Errorf("该用户不是您的有效下属")
|
||||
}
|
||||
|
||||
page := cmd.Page
|
||||
pageSize := cmd.PageSize
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
rows, total, err := s.subRepo.ListQuotaPurchasesByParentAndChild(ctx, cmd.ParentUserID, cmd.ChildUserID, pageSize, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]responses.ChildQuotaPurchaseItem, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, responses.ChildQuotaPurchaseItem{
|
||||
ID: row.ID,
|
||||
ProductID: row.ProductID,
|
||||
CallCount: row.CallCount,
|
||||
UnitPrice: row.UnitPrice.StringFixed(2),
|
||||
TotalAmount: row.TotalAmount.StringFixed(2),
|
||||
BusinessRef: row.BusinessRef,
|
||||
CreatedAt: row.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return &responses.ChildQuotaPurchaseListResponse{
|
||||
Total: total,
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListChildQuotaAccounts 下属额度账户
|
||||
func (s *SubordinateApplicationServiceImpl) ListChildQuotaAccounts(ctx context.Context, cmd *commands.ListChildQuotaAccountsCommand) ([]responses.ChildQuotaAccountItem, error) {
|
||||
lnk, err := s.subRepo.FindLinkByParentAndChild(ctx, cmd.ParentUserID, cmd.ChildUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if lnk == nil || lnk.Status != subentities.LinkStatusActive {
|
||||
return nil, fmt.Errorf("该用户不是您的有效下属")
|
||||
}
|
||||
|
||||
accounts, err := s.subRepo.ListQuotaAccountsByUser(ctx, cmd.ChildUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]responses.ChildQuotaAccountItem, 0, len(accounts))
|
||||
for _, account := range accounts {
|
||||
items = append(items, responses.ChildQuotaAccountItem{
|
||||
ProductID: account.ProductID,
|
||||
TotalQuota: account.TotalQuota,
|
||||
UsedQuota: account.UsedQuota,
|
||||
AvailableQuota: account.AvailableQuota,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// ListMyQuotaAccounts 查询当前用户额度账户(通用能力,适配所有用户)
|
||||
func (s *SubordinateApplicationServiceImpl) ListMyQuotaAccounts(ctx context.Context, userID string) ([]responses.ChildQuotaAccountItem, error) {
|
||||
accounts, err := s.subRepo.ListQuotaAccountsByUser(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]responses.ChildQuotaAccountItem, 0, len(accounts))
|
||||
for _, account := range accounts {
|
||||
items = append(items, responses.ChildQuotaAccountItem{
|
||||
ProductID: account.ProductID,
|
||||
TotalQuota: account.TotalQuota,
|
||||
UsedQuota: account.UsedQuota,
|
||||
AvailableQuota: account.AvailableQuota,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
@@ -51,6 +51,8 @@ type UserProfileResponse struct {
|
||||
IsCertified bool `json:"is_certified" example:"false"`
|
||||
CreatedAt time.Time `json:"created_at" example:"2024-01-01T00:00:00Z"`
|
||||
UpdatedAt time.Time `json:"updated_at" example:"2024-01-01T00:00:00Z"`
|
||||
// AccountKind standalone=普通/主站用户 subordinate=主账号邀请的下属
|
||||
AccountKind string `json:"account_kind" example:"standalone"`
|
||||
}
|
||||
|
||||
// SendCodeResponse 发送验证码响应
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"tyapi-server/internal/domains/user/entities"
|
||||
"tyapi-server/internal/domains/user/events"
|
||||
user_service "tyapi-server/internal/domains/user/services"
|
||||
"tyapi-server/internal/shared/auth"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
"tyapi-server/internal/shared/middleware"
|
||||
)
|
||||
@@ -27,6 +28,7 @@ type UserApplicationServiceImpl struct {
|
||||
contractService user_service.ContractAggregateService
|
||||
eventBus interfaces.EventBus
|
||||
jwtAuth *middleware.JWTAuthMiddleware
|
||||
accountKindProvider interfaces.AccountKindProvider
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
@@ -39,6 +41,7 @@ func NewUserApplicationService(
|
||||
contractService user_service.ContractAggregateService,
|
||||
eventBus interfaces.EventBus,
|
||||
jwtAuth *middleware.JWTAuthMiddleware,
|
||||
accountKindProvider interfaces.AccountKindProvider,
|
||||
logger *zap.Logger,
|
||||
) UserApplicationService {
|
||||
return &UserApplicationServiceImpl{
|
||||
@@ -49,6 +52,7 @@ func NewUserApplicationService(
|
||||
contractService: contractService,
|
||||
eventBus: eventBus,
|
||||
jwtAuth: jwtAuth,
|
||||
accountKindProvider: accountKindProvider,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
@@ -90,76 +94,16 @@ func (s *UserApplicationServiceImpl) LoginWithPassword(ctx context.Context, cmd
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 生成包含用户类型的token
|
||||
accessToken, err := s.jwtAuth.GenerateToken(user.ID, user.Phone, user.Phone, user.UserType)
|
||||
if err != nil {
|
||||
s.logger.Error("生成令牌失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("生成访问令牌失败")
|
||||
}
|
||||
|
||||
// 3. 如果是管理员,更新登录统计
|
||||
if user.IsAdmin() {
|
||||
if err := s.userAggregateService.UpdateLoginStats(ctx, user.ID); err != nil {
|
||||
s.logger.Error("更新登录统计失败", zap.Error(err))
|
||||
}
|
||||
// 重新获取用户信息以获取最新的登录统计
|
||||
updatedUser, err := s.userAggregateService.GetUserByID(ctx, user.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("重新获取用户信息失败", zap.Error(err))
|
||||
} else {
|
||||
user = updatedUser
|
||||
// 2. 账号类型(下属/普通)
|
||||
accountKind := auth.AccountKindStandalone
|
||||
if s.accountKindProvider != nil {
|
||||
if k, err := s.accountKindProvider.AccountKind(ctx, user.ID); err == nil && k != "" {
|
||||
accountKind = k
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 获取用户权限(仅管理员)
|
||||
var permissions []string
|
||||
if user.IsAdmin() {
|
||||
permissions, err = s.userAuthService.GetUserPermissions(ctx, user)
|
||||
if err != nil {
|
||||
s.logger.Error("获取用户权限失败", zap.Error(err))
|
||||
permissions = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 构建用户信息
|
||||
userProfile := &responses.UserProfileResponse{
|
||||
ID: user.ID,
|
||||
Phone: user.Phone,
|
||||
Username: user.Username,
|
||||
UserType: user.UserType,
|
||||
IsActive: user.Active,
|
||||
LastLoginAt: user.LastLoginAt,
|
||||
LoginCount: user.LoginCount,
|
||||
Permissions: permissions,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
|
||||
return &responses.LoginUserResponse{
|
||||
User: userProfile,
|
||||
AccessToken: accessToken,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: 86400, // 24h
|
||||
LoginMethod: "password",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LoginWithSMS 短信验证码登录
|
||||
// 业务流程:1. 验证短信验证码 2. 验证用户登录状态 3. 生成访问令牌 4. 更新登录统计 5. 获取用户权限
|
||||
func (s *UserApplicationServiceImpl) LoginWithSMS(ctx context.Context, cmd *commands.LoginWithSMSCommand) (*responses.LoginUserResponse, error) {
|
||||
// 1. 验证短信验证码
|
||||
if err := s.smsCodeService.VerifyCode(ctx, cmd.Phone, cmd.Code, entities.SMSSceneLogin); err != nil {
|
||||
return nil, fmt.Errorf("验证码错误或已过期")
|
||||
}
|
||||
|
||||
// 2. 验证用户登录状态
|
||||
user, err := s.userAuthService.ValidateUserLogin(ctx, cmd.Phone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 生成包含用户类型的token
|
||||
accessToken, err := s.jwtAuth.GenerateToken(user.ID, user.Phone, user.Phone, user.UserType)
|
||||
// 3. 生成包含用户类型的 token
|
||||
accessToken, err := s.jwtAuth.GenerateToken(user.ID, user.Phone, user.Phone, user.UserType, accountKind)
|
||||
if err != nil {
|
||||
s.logger.Error("生成令牌失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("生成访问令牌失败")
|
||||
@@ -201,6 +145,83 @@ func (s *UserApplicationServiceImpl) LoginWithSMS(ctx context.Context, cmd *comm
|
||||
Permissions: permissions,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
AccountKind: accountKind,
|
||||
}
|
||||
|
||||
return &responses.LoginUserResponse{
|
||||
User: userProfile,
|
||||
AccessToken: accessToken,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: 86400, // 24h
|
||||
LoginMethod: "password",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LoginWithSMS 短信验证码登录
|
||||
// 业务流程:1. 验证短信验证码 2. 验证用户登录状态 3. 生成访问令牌 4. 更新登录统计 5. 获取用户权限
|
||||
func (s *UserApplicationServiceImpl) LoginWithSMS(ctx context.Context, cmd *commands.LoginWithSMSCommand) (*responses.LoginUserResponse, error) {
|
||||
// 1. 验证短信验证码
|
||||
if err := s.smsCodeService.VerifyCode(ctx, cmd.Phone, cmd.Code, entities.SMSSceneLogin); err != nil {
|
||||
return nil, fmt.Errorf("验证码错误或已过期")
|
||||
}
|
||||
|
||||
// 2. 验证用户登录状态
|
||||
user, err := s.userAuthService.ValidateUserLogin(ctx, cmd.Phone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accountKind := auth.AccountKindStandalone
|
||||
if s.accountKindProvider != nil {
|
||||
if k, err := s.accountKindProvider.AccountKind(ctx, user.ID); err == nil && k != "" {
|
||||
accountKind = k
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 生成包含用户类型的 token
|
||||
accessToken, err := s.jwtAuth.GenerateToken(user.ID, user.Phone, user.Phone, user.UserType, accountKind)
|
||||
if err != nil {
|
||||
s.logger.Error("生成令牌失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("生成访问令牌失败")
|
||||
}
|
||||
|
||||
// 4. 如果是管理员,更新登录统计
|
||||
if user.IsAdmin() {
|
||||
if err := s.userAggregateService.UpdateLoginStats(ctx, user.ID); err != nil {
|
||||
s.logger.Error("更新登录统计失败", zap.Error(err))
|
||||
}
|
||||
// 重新获取用户信息以获取最新的登录统计
|
||||
updatedUser, err := s.userAggregateService.GetUserByID(ctx, user.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("重新获取用户信息失败", zap.Error(err))
|
||||
} else {
|
||||
user = updatedUser
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 获取用户权限(仅管理员)
|
||||
var permissions []string
|
||||
if user.IsAdmin() {
|
||||
permissions, err = s.userAuthService.GetUserPermissions(ctx, user)
|
||||
if err != nil {
|
||||
s.logger.Error("获取用户权限失败", zap.Error(err))
|
||||
permissions = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 构建用户信息
|
||||
userProfile := &responses.UserProfileResponse{
|
||||
ID: user.ID,
|
||||
Phone: user.Phone,
|
||||
Username: user.Username,
|
||||
UserType: user.UserType,
|
||||
IsActive: user.Active,
|
||||
LastLoginAt: user.LastLoginAt,
|
||||
LoginCount: user.LoginCount,
|
||||
Permissions: permissions,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
AccountKind: accountKind,
|
||||
}
|
||||
|
||||
return &responses.LoginUserResponse{
|
||||
@@ -262,6 +283,12 @@ func (s *UserApplicationServiceImpl) GetUserProfile(ctx context.Context, userID
|
||||
Permissions: permissions,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
AccountKind: auth.AccountKindStandalone,
|
||||
}
|
||||
if s.accountKindProvider != nil {
|
||||
if k, err := s.accountKindProvider.AccountKind(ctx, userID); err == nil && k != "" {
|
||||
userProfile.AccountKind = k
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 添加企业信息
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -38,10 +40,10 @@ type Config struct {
|
||||
TianYanCha TianYanChaConfig `mapstructure:"tianyancha"`
|
||||
Alicloud AlicloudConfig `mapstructure:"alicloud"`
|
||||
Xingwei XingweiConfig `mapstructure:"xingwei"`
|
||||
Jiguang JiguangConfig `mapstructure:"jiguang"`
|
||||
Shumai ShumaiConfig `mapstructure:"shumai"`
|
||||
Shujubao ShujubaoConfig `mapstructure:"shujubao"`
|
||||
PDFGen PDFGenConfig `mapstructure:"pdfgen"`
|
||||
Jiguang JiguangConfig `mapstructure:"jiguang"`
|
||||
Shumai ShumaiConfig `mapstructure:"shumai"`
|
||||
Shujubao ShujubaoConfig `mapstructure:"shujubao"`
|
||||
PDFGen PDFGenConfig `mapstructure:"pdfgen"`
|
||||
}
|
||||
|
||||
// ServerConfig HTTP服务器配置
|
||||
@@ -195,11 +197,44 @@ type AppConfig struct {
|
||||
Name string `mapstructure:"name"`
|
||||
Version string `mapstructure:"version"`
|
||||
Env string `mapstructure:"env"`
|
||||
// SubPortalBaseURL 子账号使用的前端基址(可与主站同域),用于邀请链接,无尾斜杠
|
||||
SubPortalBaseURL string `mapstructure:"sub_portal_base_url"`
|
||||
}
|
||||
|
||||
// APIConfig API配置
|
||||
type APIConfig struct {
|
||||
Domain string `mapstructure:"domain"`
|
||||
// PublicBaseURL 浏览器/第三方访问本 API 服务的完整基址(如 https://api.example.com 或 http://127.0.0.1:8080),无尾斜杠。
|
||||
// 用于企业全景报告 reportUrl、headless PDF 预生成等。为空时由 Domain 推导为 https://{Domain}(Domain 若已含 scheme 则沿用)。
|
||||
PublicBaseURL string `mapstructure:"public_base_url"`
|
||||
}
|
||||
|
||||
// ResolvedPublicBaseURL 由配置推导对外基址(不读环境变量)。
|
||||
func (c *APIConfig) ResolvedPublicBaseURL() string {
|
||||
u := strings.TrimSpace(c.PublicBaseURL)
|
||||
if u != "" {
|
||||
return strings.TrimRight(u, "/")
|
||||
}
|
||||
d := strings.TrimSpace(c.Domain)
|
||||
if d == "" {
|
||||
return ""
|
||||
}
|
||||
lo := strings.ToLower(d)
|
||||
if strings.HasPrefix(lo, "http://") || strings.HasPrefix(lo, "https://") {
|
||||
return strings.TrimRight(d, "/")
|
||||
}
|
||||
return "https://" + strings.TrimRight(d, "/")
|
||||
}
|
||||
|
||||
// ResolveAPIPublicBaseURL 对外 API 基址。优先环境变量 API_PUBLIC_BASE_URL,否则使用 API 配置。
|
||||
func ResolveAPIPublicBaseURL(cfg *APIConfig) string {
|
||||
if s := strings.TrimSpace(os.Getenv("API_PUBLIC_BASE_URL")); s != "" {
|
||||
return strings.TrimRight(s, "/")
|
||||
}
|
||||
if cfg == nil {
|
||||
return ""
|
||||
}
|
||||
return cfg.ResolvedPublicBaseURL()
|
||||
}
|
||||
|
||||
// SMSConfig 短信配置
|
||||
@@ -217,10 +252,10 @@ type SMSConfig struct {
|
||||
SignatureEnabled bool `mapstructure:"signature_enabled"` // 是否启用签名验证
|
||||
SignatureSecret string `mapstructure:"signature_secret"` // 签名密钥
|
||||
// 滑块验证码配置
|
||||
CaptchaEnabled bool `mapstructure:"captcha_enabled"` // 是否启用滑块验证码
|
||||
CaptchaSecret string `mapstructure:"captcha_secret"` // 阿里云验证码密钥
|
||||
CaptchaEndpoint string `mapstructure:"captcha_endpoint"` // 阿里云验证码服务Endpoint
|
||||
SceneID string `mapstructure:"scene_id"` // 阿里云验证码场景ID
|
||||
CaptchaEnabled bool `mapstructure:"captcha_enabled"` // 是否启用滑块验证码
|
||||
CaptchaSecret string `mapstructure:"captcha_secret"` // 阿里云验证码密钥
|
||||
CaptchaEndpoint string `mapstructure:"captcha_endpoint"` // 阿里云验证码服务Endpoint
|
||||
SceneID string `mapstructure:"scene_id"` // 阿里云验证码场景ID
|
||||
}
|
||||
|
||||
// SMSRateLimit 短信限流配置
|
||||
@@ -332,10 +367,10 @@ type SignConfig struct {
|
||||
// WalletConfig 钱包配置
|
||||
type WalletConfig struct {
|
||||
DefaultCreditLimit float64 `mapstructure:"default_credit_limit"`
|
||||
MinAmount string `mapstructure:"min_amount"` // 最低充值金额
|
||||
MaxAmount string `mapstructure:"max_amount"` // 最高充值金额
|
||||
MinAmount string `mapstructure:"min_amount"` // 最低充值金额
|
||||
MaxAmount string `mapstructure:"max_amount"` // 最高充值金额
|
||||
RechargeBonusEnabled bool `mapstructure:"recharge_bonus_enabled"` // 是否启用充值赠送,关闭后仅展示商务洽谈提示
|
||||
ApiStoreRechargeTip string `mapstructure:"api_store_recharge_tip"` // API 商店充值提示文案(大额/批量需求联系商务)
|
||||
ApiStoreRechargeTip string `mapstructure:"api_store_recharge_tip"` // API 商店充值提示文案(大额/批量需求联系商务)
|
||||
AliPayRechargeBonus []AliPayRechargeBonusRule `mapstructure:"alipay_recharge_bonus"`
|
||||
BalanceAlert BalanceAlertConfig `mapstructure:"balance_alert"`
|
||||
}
|
||||
@@ -578,10 +613,10 @@ type ShumaiConfig struct {
|
||||
|
||||
// ShumaiLoggingConfig 数脉日志配置
|
||||
type ShumaiLoggingConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
LogDir string `mapstructure:"log_dir"`
|
||||
UseDaily bool `mapstructure:"use_daily"`
|
||||
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
LogDir string `mapstructure:"log_dir"`
|
||||
UseDaily bool `mapstructure:"use_daily"`
|
||||
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
|
||||
LevelConfigs map[string]ShumaiLevelFileConfig `mapstructure:"level_configs"`
|
||||
}
|
||||
|
||||
@@ -605,10 +640,10 @@ type ShujubaoConfig struct {
|
||||
|
||||
// ShujubaoLoggingConfig 数据宝日志配置
|
||||
type ShujubaoLoggingConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
LogDir string `mapstructure:"log_dir"`
|
||||
UseDaily bool `mapstructure:"use_daily"`
|
||||
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
LogDir string `mapstructure:"log_dir"`
|
||||
UseDaily bool `mapstructure:"use_daily"`
|
||||
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
|
||||
LevelConfigs map[string]ShujubaoLevelFileConfig `mapstructure:"level_configs"`
|
||||
}
|
||||
|
||||
@@ -622,11 +657,11 @@ type ShujubaoLevelFileConfig struct {
|
||||
|
||||
// PDFGenConfig PDF生成服务配置
|
||||
type PDFGenConfig struct {
|
||||
DevelopmentURL string `mapstructure:"development_url"` // 开发环境服务地址
|
||||
ProductionURL string `mapstructure:"production_url"` // 生产环境服务地址
|
||||
APIPath string `mapstructure:"api_path"` // API路径
|
||||
Timeout time.Duration `mapstructure:"timeout"` // 请求超时时间
|
||||
Cache PDFGenCacheConfig `mapstructure:"cache"` // 缓存配置
|
||||
DevelopmentURL string `mapstructure:"development_url"` // 开发环境服务地址
|
||||
ProductionURL string `mapstructure:"production_url"` // 生产环境服务地址
|
||||
APIPath string `mapstructure:"api_path"` // API路径
|
||||
Timeout time.Duration `mapstructure:"timeout"` // 请求超时时间
|
||||
Cache PDFGenCacheConfig `mapstructure:"cache"` // 缓存配置
|
||||
}
|
||||
|
||||
// PDFGenCacheConfig PDF生成缓存配置
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"tyapi-server/internal/application/certification"
|
||||
"tyapi-server/internal/application/finance"
|
||||
"tyapi-server/internal/application/product"
|
||||
subordinate_app "tyapi-server/internal/application/subordinate"
|
||||
"tyapi-server/internal/application/statistics"
|
||||
"tyapi-server/internal/application/user"
|
||||
"tyapi-server/internal/config"
|
||||
@@ -27,6 +28,7 @@ import (
|
||||
finance_service "tyapi-server/internal/domains/finance/services"
|
||||
domain_product_repo "tyapi-server/internal/domains/product/repositories"
|
||||
product_service "tyapi-server/internal/domains/product/services"
|
||||
domain_subordinate_repo "tyapi-server/internal/domains/subordinate/repositories"
|
||||
statistics_service "tyapi-server/internal/domains/statistics/services"
|
||||
user_service "tyapi-server/internal/domains/user/services"
|
||||
"tyapi-server/internal/infrastructure/cache"
|
||||
@@ -35,7 +37,9 @@ import (
|
||||
certification_repo "tyapi-server/internal/infrastructure/database/repositories/certification"
|
||||
finance_repo "tyapi-server/internal/infrastructure/database/repositories/finance"
|
||||
product_repo "tyapi-server/internal/infrastructure/database/repositories/product"
|
||||
subordinate_db "tyapi-server/internal/infrastructure/database/repositories/subordinate"
|
||||
infra_events "tyapi-server/internal/infrastructure/events"
|
||||
subordinate_infra "tyapi-server/internal/infrastructure/subordinate"
|
||||
"tyapi-server/internal/infrastructure/external/alicloud"
|
||||
"tyapi-server/internal/infrastructure/external/captcha"
|
||||
"tyapi-server/internal/infrastructure/external/email"
|
||||
@@ -67,6 +71,7 @@ import (
|
||||
"tyapi-server/internal/shared/hooks"
|
||||
sharedhttp "tyapi-server/internal/shared/http"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
"tyapi-server/internal/shared/ipgeo"
|
||||
"tyapi-server/internal/shared/logger"
|
||||
"tyapi-server/internal/shared/metrics"
|
||||
"tyapi-server/internal/shared/middleware"
|
||||
@@ -87,6 +92,7 @@ import (
|
||||
api_app "tyapi-server/internal/application/api"
|
||||
domain_api_repo "tyapi-server/internal/domains/api/repositories"
|
||||
api_services "tyapi-server/internal/domains/api/services"
|
||||
api_processors "tyapi-server/internal/domains/api/services/processors"
|
||||
finance_services "tyapi-server/internal/domains/finance/services"
|
||||
product_services "tyapi-server/internal/domains/product/services"
|
||||
domain_statistics_repo "tyapi-server/internal/domains/statistics/repositories"
|
||||
@@ -239,19 +245,19 @@ func NewContainer() *Container {
|
||||
},
|
||||
// 短信服务
|
||||
sms.NewAliSMSService,
|
||||
// 验证码服务
|
||||
fx.Annotate(
|
||||
func(cfg *config.Config) *captcha.CaptchaService {
|
||||
return captcha.NewCaptchaService(captcha.CaptchaConfig{
|
||||
AccessKeyID: cfg.SMS.AccessKeyID,
|
||||
AccessKeySecret: cfg.SMS.AccessKeySecret,
|
||||
EndpointURL: cfg.SMS.CaptchaEndpoint,
|
||||
SceneID: cfg.SMS.SceneID,
|
||||
EncryptKey: cfg.SMS.CaptchaSecret, // 加密模式 ekey(Base64 编码的 32 字节)
|
||||
})
|
||||
},
|
||||
fx.ResultTags(`name:"captchaService"`),
|
||||
),
|
||||
// 验证码服务
|
||||
fx.Annotate(
|
||||
func(cfg *config.Config) *captcha.CaptchaService {
|
||||
return captcha.NewCaptchaService(captcha.CaptchaConfig{
|
||||
AccessKeyID: cfg.SMS.AccessKeyID,
|
||||
AccessKeySecret: cfg.SMS.AccessKeySecret,
|
||||
EndpointURL: cfg.SMS.CaptchaEndpoint,
|
||||
SceneID: cfg.SMS.SceneID,
|
||||
EncryptKey: cfg.SMS.CaptchaSecret, // 加密模式 ekey(Base64 编码的 32 字节)
|
||||
})
|
||||
},
|
||||
fx.ResultTags(`name:"captchaService"`),
|
||||
),
|
||||
// 邮件服务
|
||||
fx.Annotate(
|
||||
func(cfg *config.Config, logger *zap.Logger) *email.QQEmailService {
|
||||
@@ -411,13 +417,11 @@ func NewContainer() *Container {
|
||||
)
|
||||
},
|
||||
// AlicloudService - 阿里云服务
|
||||
func(cfg *config.Config) *alicloud.AlicloudService {
|
||||
return alicloud.NewAlicloudService(
|
||||
cfg.Alicloud.Host,
|
||||
cfg.Alicloud.AppCode,
|
||||
)
|
||||
func(cfg *config.Config) (*alicloud.AlicloudService, error) {
|
||||
return alicloud.NewAlicloudServiceWithConfig(cfg)
|
||||
},
|
||||
sharedhttp.NewGinRouter,
|
||||
ipgeo.NewLocator,
|
||||
),
|
||||
|
||||
// 中间件组件
|
||||
@@ -428,7 +432,7 @@ func NewContainer() *Container {
|
||||
middleware.NewCORSMiddleware,
|
||||
middleware.NewRateLimitMiddleware,
|
||||
// 每日限流中间件
|
||||
func(cfg *config.Config, redis *redis.Client, response interfaces.ResponseBuilder, logger *zap.Logger) *middleware.DailyRateLimitMiddleware {
|
||||
func(cfg *config.Config, redis *redis.Client, db *gorm.DB, response interfaces.ResponseBuilder, logger *zap.Logger) *middleware.DailyRateLimitMiddleware {
|
||||
limitConfig := middleware.DailyRateLimitConfig{
|
||||
MaxRequestsPerDay: cfg.DailyRateLimit.MaxRequestsPerDay,
|
||||
MaxRequestsPerIP: cfg.DailyRateLimit.MaxRequestsPerIP,
|
||||
@@ -452,7 +456,7 @@ func NewContainer() *Container {
|
||||
// 排除域名配置
|
||||
ExcludeDomains: cfg.DailyRateLimit.ExcludeDomains,
|
||||
}
|
||||
return middleware.NewDailyRateLimitMiddleware(cfg, redis, response, logger, limitConfig)
|
||||
return middleware.NewDailyRateLimitMiddleware(cfg, redis, db, response, logger, limitConfig)
|
||||
},
|
||||
NewRequestLoggerMiddlewareWrapper,
|
||||
middleware.NewJWTAuthMiddleware,
|
||||
@@ -666,6 +670,14 @@ func NewContainer() *Container {
|
||||
),
|
||||
),
|
||||
|
||||
// 下属账号仓储
|
||||
fx.Provide(
|
||||
fx.Annotate(
|
||||
subordinate_db.NewGormSubordinateRepository,
|
||||
fx.As(new(domain_subordinate_repo.SubordinateRepository)),
|
||||
),
|
||||
),
|
||||
|
||||
// 统计域仓储层
|
||||
fx.Provide(
|
||||
fx.Annotate(
|
||||
@@ -799,6 +811,7 @@ func NewContainer() *Container {
|
||||
subscriptionService *product_services.ProductSubscriptionService,
|
||||
exportManager *export.ExportManager,
|
||||
balanceAlertService finance_services.BalanceAlertService,
|
||||
subordinateRepo domain_subordinate_repo.SubordinateRepository,
|
||||
) api_app.ApiApplicationService {
|
||||
return api_app.NewApiApplicationService(
|
||||
apiCallService,
|
||||
@@ -817,6 +830,7 @@ func NewContainer() *Container {
|
||||
subscriptionService,
|
||||
exportManager,
|
||||
balanceAlertService,
|
||||
subordinateRepo,
|
||||
)
|
||||
},
|
||||
fx.As(new(api_app.ApiApplicationService)),
|
||||
@@ -889,6 +903,21 @@ func NewContainer() *Container {
|
||||
user.NewUserApplicationService,
|
||||
fx.As(new(user.UserApplicationService)),
|
||||
),
|
||||
// 下属:账号类型供 JWT / 资料
|
||||
fx.Annotate(
|
||||
subordinate_infra.NewAccountKindProviderImpl,
|
||||
fx.As(new(interfaces.AccountKindProvider)),
|
||||
),
|
||||
// 下属:禁止子账号自助订
|
||||
fx.Annotate(
|
||||
subordinate_app.NewBlockSelfSubscribeForSubordinate,
|
||||
fx.As(new(product.SelfSubscribePolicy)),
|
||||
),
|
||||
// 下属:邀请/划款/代配
|
||||
fx.Annotate(
|
||||
subordinate_app.NewSubordinateApplicationService,
|
||||
fx.As(new(subordinate_app.SubordinateApplicationService)),
|
||||
),
|
||||
// 认证应用服务 - 绑定到接口
|
||||
fx.Annotate(
|
||||
func(
|
||||
@@ -905,6 +934,8 @@ func NewContainer() *Container {
|
||||
apiUserAggregateService api_services.ApiUserAggregateService,
|
||||
enterpriseInfoSubmitRecordService *certification_service.EnterpriseInfoSubmitRecordService,
|
||||
ocrService sharedOCR.OCRService,
|
||||
subordinateRepo domain_subordinate_repo.SubordinateRepository,
|
||||
walletRepo domain_finance_repo.WalletRepository,
|
||||
txManager *shared_database.TransactionManager,
|
||||
logger *zap.Logger,
|
||||
cfg *config.Config,
|
||||
@@ -923,6 +954,8 @@ func NewContainer() *Container {
|
||||
apiUserAggregateService,
|
||||
enterpriseInfoSubmitRecordService,
|
||||
ocrService,
|
||||
subordinateRepo,
|
||||
walletRepo,
|
||||
txManager,
|
||||
logger,
|
||||
cfg,
|
||||
@@ -1208,6 +1241,18 @@ func NewContainer() *Container {
|
||||
return cacheManager, nil
|
||||
},
|
||||
),
|
||||
// 企业全景报告 PDF 异步预生成(依赖 PDF 缓存目录与公网可访问基址)
|
||||
// 同时以 processors.QYGLReportPDFScheduler 注入 ApiRequestService
|
||||
fx.Provide(
|
||||
fx.Annotate(
|
||||
func(cfg *config.Config, logger *zap.Logger, cache *pdf.PDFCacheManager) *pdf.QYGLReportPDFPregen {
|
||||
base := config.ResolveAPIPublicBaseURL(&cfg.API)
|
||||
return pdf.NewQYGLReportPDFPregen(logger, cache, base)
|
||||
},
|
||||
fx.As(new(api_processors.QYGLReportPDFScheduler)),
|
||||
fx.As(fx.Self()), // 同时保留 *pdf.QYGLReportPDFPregen,供 QYGLReportHandler 等注入
|
||||
),
|
||||
),
|
||||
// 本地文件存储服务
|
||||
fx.Provide(
|
||||
func(logger *zap.Logger) *storage.LocalFileStorageService {
|
||||
@@ -1230,6 +1275,7 @@ func NewContainer() *Container {
|
||||
fx.Provide(
|
||||
// 用户HTTP处理器
|
||||
handlers.NewUserHandler,
|
||||
handlers.NewSubordinateHandler,
|
||||
// 认证HTTP处理器
|
||||
handlers.NewCertificationHandler,
|
||||
// 财务HTTP处理器
|
||||
@@ -1244,6 +1290,8 @@ func NewContainer() *Container {
|
||||
handlers.NewApiHandler,
|
||||
// 统计HTTP处理器
|
||||
handlers.NewStatisticsHandler,
|
||||
// 管理员安全HTTP处理器
|
||||
handlers.NewAdminSecurityHandler,
|
||||
// 文章HTTP处理器
|
||||
func(
|
||||
appService article.ArticleApplicationService,
|
||||
@@ -1311,6 +1359,7 @@ func NewContainer() *Container {
|
||||
fx.Provide(
|
||||
// 用户路由
|
||||
routes.NewUserRoutes,
|
||||
routes.NewSubordinateRoutes,
|
||||
// 验证码路由
|
||||
routes.NewCaptchaRoutes,
|
||||
// 认证路由
|
||||
@@ -1335,6 +1384,8 @@ func NewContainer() *Container {
|
||||
routes.NewApiRoutes,
|
||||
// 统计路由
|
||||
routes.NewStatisticsRoutes,
|
||||
// 管理员安全路由
|
||||
routes.NewAdminSecurityRoutes,
|
||||
// PDFG路由
|
||||
routes.NewPDFGRoutes,
|
||||
// 企业报告页面路由
|
||||
@@ -1441,6 +1492,7 @@ func RegisterMiddlewares(
|
||||
func RegisterRoutes(
|
||||
router *sharedhttp.GinRouter,
|
||||
userRoutes *routes.UserRoutes,
|
||||
subordinateRoutes *routes.SubordinateRoutes,
|
||||
captchaRoutes *routes.CaptchaRoutes,
|
||||
certificationRoutes *routes.CertificationRoutes,
|
||||
financeRoutes *routes.FinanceRoutes,
|
||||
@@ -1453,6 +1505,7 @@ func RegisterRoutes(
|
||||
announcementRoutes *routes.AnnouncementRoutes,
|
||||
apiRoutes *routes.ApiRoutes,
|
||||
statisticsRoutes *routes.StatisticsRoutes,
|
||||
adminSecurityRoutes *routes.AdminSecurityRoutes,
|
||||
pdfgRoutes *routes.PDFGRoutes,
|
||||
qyglReportRoutes *routes.QYGLReportRoutes,
|
||||
jwtAuth *middleware.JWTAuthMiddleware,
|
||||
@@ -1467,6 +1520,7 @@ func RegisterRoutes(
|
||||
|
||||
// 所有域名路由路由
|
||||
userRoutes.Register(router)
|
||||
subordinateRoutes.Register(router)
|
||||
captchaRoutes.Register(router)
|
||||
certificationRoutes.Register(router)
|
||||
financeRoutes.Register(router)
|
||||
@@ -1479,6 +1533,7 @@ func RegisterRoutes(
|
||||
articleRoutes.Register(router)
|
||||
announcementRoutes.Register(router)
|
||||
statisticsRoutes.Register(router)
|
||||
adminSecurityRoutes.Register(router)
|
||||
pdfgRoutes.Register(router)
|
||||
qyglReportRoutes.Register(router)
|
||||
|
||||
|
||||
@@ -72,6 +72,11 @@ type IVYZ81NCReq struct {
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
}
|
||||
type IVYZ2MN6Req struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||
}
|
||||
type IVYZ9363Req struct {
|
||||
ManName string `json:"man_name" validate:"required,min=1,validName"`
|
||||
ManIDCard string `json:"man_id_card" validate:"required,validIDCard"`
|
||||
@@ -118,7 +123,10 @@ type QYGLUY3SReq struct {
|
||||
EntRegno string `json:"ent_reg_no" validate:"omitempty"`
|
||||
EntCode string `json:"ent_code" validate:"omitempty,validUSCI"`
|
||||
}
|
||||
|
||||
type QYGLJ1U9Req struct {
|
||||
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
|
||||
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
||||
}
|
||||
type JRZQOCRYReq struct {
|
||||
PhotoData string `json:"photo_data" validate:"required,validBase64Image"`
|
||||
}
|
||||
@@ -407,7 +415,7 @@ type QCXGP00WReq struct {
|
||||
VinCode string `json:"vin_code" validate:"required"`
|
||||
PlateNo string `json:"plate_no" validate:"omitempty"`
|
||||
ReturnURL string `json:"return_url" validate:"required,validReturnURL"`
|
||||
VlPhotoData string `json:"vlphoto_data" validate:"omitempty,validBase64Image"`
|
||||
VlPhotoData string `json:"vlphoto_data" validate:"required,validBase64Image"`
|
||||
}
|
||||
|
||||
type QCXG4D2EReq struct {
|
||||
@@ -491,6 +499,12 @@ type IVYZ7F3AReq struct {
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||
}
|
||||
type IVYZRAX1Req struct {
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||
}
|
||||
|
||||
type IVYZ3P9MReq struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
@@ -613,6 +627,55 @@ type QYGLJ0Q1Req struct {
|
||||
EntName string `json:"ent_name" validate:"omitempty,min=1,validEnterpriseName"`
|
||||
EntCode string `json:"ent_code" validate:"omitempty,validUSCI"`
|
||||
}
|
||||
|
||||
type IVYZ18HYReq struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
MaritalType string `json:"marital_type" validate:"required" oneof=10 20 30 40`
|
||||
AuthAuthorizeFileBase64 string `json:"auth_authorize_file_base64" validate:"required,validBase64"`
|
||||
AuthAuthorizeFileCode string `json:"auth_authorize_file_code" validate:"required"`
|
||||
AuthDate string `json:"auth_date" validate:"omitempty"`
|
||||
}
|
||||
|
||||
type IVYZ38SRReq struct {
|
||||
ManName string `json:"man_name" validate:"required,min=1,validName"`
|
||||
ManIDCard string `json:"man_id_card" validate:"required,validIDCard"`
|
||||
WomanName string `json:"woman_name" validate:"required,min=1,validName"`
|
||||
WomanIDCard string `json:"woman_id_card" validate:"required,validIDCard"`
|
||||
}
|
||||
|
||||
type IVYZ5E22Req struct {
|
||||
ManName string `json:"man_name" validate:"required,min=1,validName"`
|
||||
ManIDCard string `json:"man_id_card" validate:"required,validIDCard"`
|
||||
WomanName string `json:"woman_name" validate:"required,min=1,validName"`
|
||||
WomanIDCard string `json:"woman_id_card" validate:"required,validIDCard"`
|
||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||
}
|
||||
type IVYZ48SRReq struct {
|
||||
ManName string `json:"man_name" validate:"required,min=1,validName"`
|
||||
ManIDCard string `json:"man_id_card" validate:"required,validIDCard"`
|
||||
WomanName string `json:"woman_name" validate:"required,min=1,validName"`
|
||||
WomanIDCard string `json:"woman_id_card" validate:"required,validIDCard"`
|
||||
MaritalType string `json:"marital_type" validate:"required" oneof=10 20 30 40`
|
||||
AuthAuthorizeFileCode string `json:"auth_authorize_file_code" validate:"required"`
|
||||
}
|
||||
type IVYZ28HYReq struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
}
|
||||
type FLXGDJG3Req struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
}
|
||||
type QYGLDJ12Req struct {
|
||||
EntName string `json:"ent_name" validate:"omitempty,min=1,validEnterpriseName"`
|
||||
EntCode string `json:"ent_code" validate:"omitempty,validUSCI"`
|
||||
EntRegNo string `json:"ent_reg_no" validate:"omitempty"`
|
||||
}
|
||||
type QYGLDJ33Req struct {
|
||||
EntName string `json:"ent_name" validate:"omitempty,min=1,validEnterpriseName"`
|
||||
EntCode string `json:"ent_code" validate:"omitempty,validUSCI"`
|
||||
EntRegNo string `json:"ent_reg_no" validate:"omitempty"`
|
||||
}
|
||||
type YYSY6D9AReq struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
@@ -636,6 +699,13 @@ type FLXG9C1DReq struct {
|
||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||
}
|
||||
|
||||
type DWBG5SAMReq struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
AuthorizationURL string `json:"authorization_url" validate:"required,authorization_url"`
|
||||
}
|
||||
|
||||
// 法院被执行人限高版
|
||||
type FLXG3A9BReq struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
@@ -989,6 +1059,13 @@ type IVYZA1B3Req struct {
|
||||
PhotoData string `json:"photo_data" validate:"required,validBase64Image"`
|
||||
}
|
||||
|
||||
type IVYZFIC1Req struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
PhotoData string `json:"photo_data" validate:"omitempty,validBase64Image"`
|
||||
ImageUrl string `json:"+" validate:"omitempty,url"`
|
||||
}
|
||||
|
||||
type IVYZC4R9Req struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"tyapi-server/internal/application/api/commands"
|
||||
"tyapi-server/internal/config"
|
||||
appconfig "tyapi-server/internal/config"
|
||||
api_repositories "tyapi-server/internal/domains/api/repositories"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/domains/api/services/processors/comb"
|
||||
@@ -50,7 +50,7 @@ type ApiRequestService struct {
|
||||
validator interfaces.RequestValidator
|
||||
processorDeps *processors.ProcessorDependencies
|
||||
combService *comb.CombService
|
||||
config *config.Config
|
||||
config *appconfig.Config
|
||||
|
||||
reportRepo api_repositories.ReportRepository
|
||||
}
|
||||
@@ -68,7 +68,7 @@ func NewApiRequestService(
|
||||
shumaiService *shumai.ShumaiService,
|
||||
validator interfaces.RequestValidator,
|
||||
productManagementService *services.ProductManagementService,
|
||||
cfg *config.Config,
|
||||
cfg *appconfig.Config,
|
||||
) *ApiRequestService {
|
||||
return NewApiRequestServiceWithRepos(
|
||||
westDexService,
|
||||
@@ -85,6 +85,7 @@ func NewApiRequestService(
|
||||
productManagementService,
|
||||
cfg,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -102,12 +103,18 @@ func NewApiRequestServiceWithRepos(
|
||||
shumaiService *shumai.ShumaiService,
|
||||
validator interfaces.RequestValidator,
|
||||
productManagementService *services.ProductManagementService,
|
||||
cfg *config.Config,
|
||||
cfg *appconfig.Config,
|
||||
reportRepo api_repositories.ReportRepository,
|
||||
qyglReportPDFScheduler processors.QYGLReportPDFScheduler,
|
||||
) *ApiRequestService {
|
||||
// 创建组合包服务
|
||||
combService := comb.NewCombService(productManagementService)
|
||||
|
||||
apiPublicBase := ""
|
||||
if cfg != nil {
|
||||
apiPublicBase = appconfig.ResolveAPIPublicBaseURL(&cfg.API)
|
||||
}
|
||||
|
||||
// 创建处理器依赖容器
|
||||
processorDeps := processors.NewProcessorDependencies(
|
||||
westDexService,
|
||||
@@ -123,22 +130,24 @@ func NewApiRequestServiceWithRepos(
|
||||
validator,
|
||||
combService,
|
||||
reportRepo,
|
||||
qyglReportPDFScheduler,
|
||||
apiPublicBase,
|
||||
)
|
||||
|
||||
// 统一注册所有处理器
|
||||
registerAllProcessors(combService)
|
||||
|
||||
return &ApiRequestService{
|
||||
westDexService: westDexService,
|
||||
muziService: muziService,
|
||||
yushanService: yushanService,
|
||||
tianYanChaService: tianYanChaService,
|
||||
alicloudService: alicloudService,
|
||||
validator: validator,
|
||||
processorDeps: processorDeps,
|
||||
combService: combService,
|
||||
config: cfg,
|
||||
reportRepo: reportRepo,
|
||||
westDexService: westDexService,
|
||||
muziService: muziService,
|
||||
yushanService: yushanService,
|
||||
tianYanChaService: tianYanChaService,
|
||||
alicloudService: alicloudService,
|
||||
validator: validator,
|
||||
processorDeps: processorDeps,
|
||||
combService: combService,
|
||||
config: cfg,
|
||||
reportRepo: reportRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +180,7 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"FLXG7E8F": flxg.ProcessFLXG7E8FRequest,
|
||||
"FLXG3A9B": flxg.ProcessFLXG3A9BRequest,
|
||||
"FLXGK5D2": flxg.ProcessFLXGK5D2Request,
|
||||
"FLXGDJG3": flxg.ProcessFLXGDJG3Request, //董监高司法综合信息核验
|
||||
// JRZQ系列处理器
|
||||
"JRZQ8203": jrzq.ProcessJRZQ8203Request,
|
||||
"JRZQ0A03": jrzq.ProcessJRZQ0A03Request,
|
||||
@@ -236,37 +246,41 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"QYGLJ1U9": qygl.ProcessQYGLJ1U9Request, //企业全景报告(聚合 QYGLUY3S/QYGLJ0Q1/QYGL5S1I)
|
||||
"QYGLJ0Q1": qygl.ProcessQYGLJ0Q1Request, //企业股权结构全景查询
|
||||
"QYGLUY3S": qygl.ProcessQYGLUY3SRequest, //企业经营状态全景查询
|
||||
"YYSY35TA": yysy.ProcessYYSY35TARequest, //运营商归属地数卖
|
||||
"QYGLDJ12": qygl.ProcessQYGLDJ12Request, //企业年报信息核验
|
||||
"QYGL8848": qygl.ProcessQYGL8848Request, //企业税收违法核查
|
||||
"QYGLDJ33": qygl.ProcessQYGLDJ33Request, //企业年报信息核验
|
||||
|
||||
// YYSY系列处理器
|
||||
"YYSYD50F": yysy.ProcessYYSYD50FRequest,
|
||||
"YYSY09CD": yysy.ProcessYYSY09CDRequest,
|
||||
"YYSY4B21": yysy.ProcessYYSY4B21Request,
|
||||
"YYSY4B37": yysy.ProcessYYSY4B37Request,
|
||||
"YYSY6F2E": yysy.ProcessYYSY6F2ERequest,
|
||||
"YYSYBE08": yysy.ProcessYYSYBE08Request,
|
||||
"YYSYF7DB": yysy.ProcessYYSYF7DBRequest,
|
||||
"YYSY4F2E": yysy.ProcessYYSY4F2ERequest,
|
||||
"YYSY8B1C": yysy.ProcessYYSY8B1CRequest,
|
||||
"YYSY6D9A": yysy.ProcessYYSY6D9ARequest,
|
||||
"YYSY3E7F": yysy.ProcessYYSY3E7FRequest,
|
||||
"YYSY8F3A": yysy.ProcessYYSY8F3ARequest,
|
||||
"YYSY9A1B": yysy.ProcessYYSY9A1BRequest,
|
||||
"YYSY8C2D": yysy.ProcessYYSY8C2DRequest,
|
||||
"YYSY7D3E": yysy.ProcessYYSY7D3ERequest,
|
||||
"YYSY9E4A": yysy.ProcessYYSY9E4ARequest,
|
||||
"YYSY9F1B": yysy.ProcessYYSY9F1BYequest,
|
||||
"YYSY6F2B": yysy.ProcessYYSY6F2BRequest,
|
||||
"YYSY3M8S": yysy.ProcessYYSY3M8SRequest, //运营商二要素查询
|
||||
"YYSYC4R9": yysy.ProcessYYSYC4R9Request, //运营商三要素详版查询
|
||||
"YYSYH6D2": yysy.ProcessYYSYH6D2Request, //运营商三要素简版查询
|
||||
"YYSYP0T4": yysy.ProcessYYSYP0T4Request, //在网时长查询
|
||||
"YYSYE7V5": yysy.ProcessYYSYE7V5Request, //手机在网状态查询
|
||||
"YYSYS9W1": yysy.ProcessYYSYS9W1Request, //手机携号转网查询
|
||||
"YYSYK8R3": yysy.ProcessYYSYK8R3Request, //手机空号检测查询
|
||||
"YYSYH6F3": yysy.ProcessYYSYH6F3Request, //运营商三要素即时版查询
|
||||
"YYSYK9R4": yysy.ProcessYYSYK9R4Request, //全网手机三要素验证1979周更新版
|
||||
"YYSYF2T7": yysy.ProcessYYSYF2T7Request, //手机二次放号检测查询
|
||||
"YYSY35TA": yysy.ProcessYYSY35TARequest, //运营商归属地数卖
|
||||
"YYSYD50F": yysy.ProcessYYSYD50FRequest,
|
||||
"YYSY09CD": yysy.ProcessYYSY09CDRequest,
|
||||
"YYSY4B21": yysy.ProcessYYSY4B21Request,
|
||||
"YYSY4B37": yysy.ProcessYYSY4B37Request,
|
||||
"YYSY6F2E": yysy.ProcessYYSY6F2ERequest,
|
||||
"YYSYBE08": yysy.ProcessYYSYBE08Request,
|
||||
"YYSYBE08TEST": yysy.ProcessYYSYBE08testRequest, // 二要素(阿里云市场),与 YYSYBE08 入参一致
|
||||
"YYSYF7DB": yysy.ProcessYYSYF7DBRequest,
|
||||
"YYSY4F2E": yysy.ProcessYYSY4F2ERequest,
|
||||
"YYSY8B1C": yysy.ProcessYYSY8B1CRequest,
|
||||
"YYSY6D9A": yysy.ProcessYYSY6D9ARequest,
|
||||
"YYSY3E7F": yysy.ProcessYYSY3E7FRequest,
|
||||
"YYSY8F3A": yysy.ProcessYYSY8F3ARequest,
|
||||
"YYSY9A1B": yysy.ProcessYYSY9A1BRequest,
|
||||
"YYSY8C2D": yysy.ProcessYYSY8C2DRequest,
|
||||
"YYSY7D3E": yysy.ProcessYYSY7D3ERequest,
|
||||
"YYSY9E4A": yysy.ProcessYYSY9E4ARequest,
|
||||
"YYSY9F1B": yysy.ProcessYYSY9F1BYequest,
|
||||
"YYSY6F2B": yysy.ProcessYYSY6F2BRequest,
|
||||
"YYSY3M8S": yysy.ProcessYYSY3M8SRequest, //运营商二要素查询
|
||||
"YYSYC4R9": yysy.ProcessYYSYC4R9Request, //运营商三要素详版查询
|
||||
"YYSYH6D2": yysy.ProcessYYSYH6D2Request, //运营商三要素简版查询
|
||||
"YYSYP0T4": yysy.ProcessYYSYP0T4Request, //在网时长查询
|
||||
"YYSYE7V5": yysy.ProcessYYSYE7V5Request, //手机在网状态查询
|
||||
"YYSYS9W1": yysy.ProcessYYSYS9W1Request, //手机携号转网查询
|
||||
"YYSYK8R3": yysy.ProcessYYSYK8R3Request, //手机空号检测查询
|
||||
"YYSYH6F3": yysy.ProcessYYSYH6F3Request, //运营商三要素即时版查询
|
||||
"YYSYK9R4": yysy.ProcessYYSYK9R4Request, //全网手机三要素验证1979周更新版
|
||||
"YYSYF2T7": yysy.ProcessYYSYF2T7Request, //手机二次放号检测查询
|
||||
|
||||
// IVYZ系列处理器
|
||||
"IVYZ0B03": ivyz.ProcessIVYZ0B03Request,
|
||||
@@ -288,6 +302,7 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"IVYZ3A7F": ivyz.ProcessIVYZ3A7FRequest,
|
||||
"IVYZ9D2E": ivyz.ProcessIVYZ9D2ERequest,
|
||||
"IVYZ81NC": ivyz.ProcessIVYZ81NCRequest,
|
||||
"IVYZ2MN6": ivyz.ProcessIVYZ2MN6Request,
|
||||
"IVYZ6G7H": ivyz.ProcessIVYZ6G7HRequest,
|
||||
"IVYZ8I9J": ivyz.ProcessIVYZ8I9JRequest,
|
||||
"IVYZ9K2L": ivyz.ProcessIVYZ9K2LRequest,
|
||||
@@ -304,11 +319,20 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"IVYZ1J7H": ivyz.ProcessIVYZ1J7HRequest, //行驶证核查v2
|
||||
"IVYZ9K7F": ivyz.ProcessIVYZ9K7FRequest, //身份证实名认证即时版
|
||||
"IVYZA1B3": ivyz.ProcessIVYZA1B3Request, //公安三要素人脸识别
|
||||
"IVYZFIC1": ivyz.ProcessIVYZFIC1Request, //人脸身份证比对(数脉)
|
||||
"IVYZN2P8": ivyz.ProcessIVYZN2P8Request, //身份证实名认证政务版
|
||||
"IVYZX5QZ": ivyz.ProcessIVYZX5QZRequest, //活体检测
|
||||
"IVYZX5Q2": ivyz.ProcessIVYZX5Q2Request, //活体识别步骤二
|
||||
"IVYZOCR1": ivyz.ProcessIVYZOCR1Request, //身份证OCR
|
||||
"IVYZOCR2": ivyz.ProcessIVYZOCR2Request, //身份证OCR2数卖
|
||||
"IVYZ18HY": ivyz.ProcessIVYZ18HYRequest, //婚姻状况核验V2(单人)
|
||||
"IVYZ28HY": ivyz.ProcessIVYZ28HYRequest, //婚姻状况核验(单人)
|
||||
"IVYZ38SR": ivyz.ProcessIVYZ38SRRequest, //婚姻状态核验(双人)
|
||||
"IVYZ48SR": ivyz.ProcessIVYZ48SRRequest, //婚姻状态核验V2(双人)
|
||||
"IVYZ5E22": ivyz.ProcessIVYZ5E22Request, //双人婚姻评估查询zhicha版本
|
||||
"IVYZRAX1": ivyz.ProcessIVYZRAX1Request, //融安信用分
|
||||
"IVYZRAX2": ivyz.ProcessIVYZRAX2Request,//融御反欺诈分
|
||||
|
||||
|
||||
// COMB系列处理器 - 只注册有自定义逻辑的组合包
|
||||
"COMB86PM": comb.ProcessCOMB86PMRequest, // 有自定义逻辑:重命名ApiCode
|
||||
@@ -347,6 +371,7 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"DWBG6A2C": dwbg.ProcessDWBG6A2CRequest,
|
||||
"DWBG8B4D": dwbg.ProcessDWBG8B4DRequest,
|
||||
"DWBG7F3A": dwbg.ProcessDWBG7F3ARequest,
|
||||
"DWBG5SAM": dwbg.ProcessDWBG5SAMRequest,
|
||||
|
||||
// FLXG系列处理器 - 风险管控 (包含原FXHY功能)
|
||||
"FLXG8B4D": flxg.ProcessFLXG8B4DRequest,
|
||||
|
||||
@@ -76,192 +76,208 @@ func (s *FormConfigServiceImpl) GetFormConfig(ctx context.Context, apiCode strin
|
||||
func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string) (interface{}, error) {
|
||||
// 建立API代码到DTO结构体的映射
|
||||
dtoMap := map[string]interface{}{
|
||||
"IVYZ9363": &dto.IVYZ9363Req{},
|
||||
"IVYZ385E": &dto.IVYZ385EReq{},
|
||||
"IVYZ5733": &dto.IVYZ5733Req{},
|
||||
"FLXG3D56": &dto.FLXG3D56Req{},
|
||||
"FLXG75FE": &dto.FLXG75FEReq{},
|
||||
"FLXG0V3B": &dto.FLXG0V3BReq{},
|
||||
"FLXG0V4B": &dto.FLXG0V4BReq{},
|
||||
"FLXG54F5": &dto.FLXG54F5Req{},
|
||||
"FLXG162A": &dto.FLXG162AReq{},
|
||||
"FLXG0687": &dto.FLXG0687Req{},
|
||||
"FLXGBC21": &dto.FLXGBC21Req{},
|
||||
"FLXG970F": &dto.FLXG970FReq{},
|
||||
"FLXG5876": &dto.FLXG5876Req{},
|
||||
"FLXG9687": &dto.FLXG9687Req{},
|
||||
"FLXGC9D1": &dto.FLXGC9D1Req{},
|
||||
"FLXGCA3D": &dto.FLXGCA3DReq{},
|
||||
"FLXGDEC7": &dto.FLXGDEC7Req{},
|
||||
"JRZQ0A03": &dto.JRZQ0A03Req{},
|
||||
"JRZQ4AA8": &dto.JRZQ4AA8Req{},
|
||||
"JRZQ8203": &dto.JRZQ8203Req{},
|
||||
"JRZQDCBE": &dto.JRZQDCBEReq{},
|
||||
"QYGL2ACD": &dto.QYGL2ACDReq{},
|
||||
"QYGL6F2D": &dto.QYGL6F2DReq{},
|
||||
"QYGL45BD": &dto.QYGL45BDReq{},
|
||||
"QYGL8261": &dto.QYGL8261Req{},
|
||||
"QYGL8271": &dto.QYGL8271Req{},
|
||||
"QYGLB4C0": &dto.QYGLB4C0Req{},
|
||||
"QYGL23T7": &dto.QYGL23T7Req{},
|
||||
"QYGL5A3C": &dto.QYGL5A3CReq{},
|
||||
"QYGL8B4D": &dto.QYGL8B4DReq{},
|
||||
"QYGL9E2F": &dto.QYGL9E2FReq{},
|
||||
"QYGL7C1A": &dto.QYGL7C1AReq{},
|
||||
"QYGL3F8E": &dto.QYGL3F8EReq{},
|
||||
"YYSY4B37": &dto.YYSY4B37Req{},
|
||||
"YYSY4B21": &dto.YYSY4B21Req{},
|
||||
"YYSY6F2E": &dto.YYSY6F2EReq{},
|
||||
"YYSY09CD": &dto.YYSY09CDReq{},
|
||||
"IVYZ0B03": &dto.IVYZ0B03Req{},
|
||||
"YYSYBE08": &dto.YYSYBE08Req{},
|
||||
"YYSYD50F": &dto.YYSYD50FReq{},
|
||||
"YYSYF7DB": &dto.YYSYF7DBReq{},
|
||||
"IVYZ9A2B": &dto.IVYZ9A2BReq{},
|
||||
"IVYZ7F2A": &dto.IVYZ7F2AReq{},
|
||||
"IVYZ4E8B": &dto.IVYZ4E8BReq{},
|
||||
"IVYZ1C9D": &dto.IVYZ1C9DReq{},
|
||||
"IVYZGZ08": &dto.IVYZGZ08Req{},
|
||||
"FLXG8A3F": &dto.FLXG8A3FReq{},
|
||||
"FLXG5B2E": &dto.FLXG5B2EReq{},
|
||||
"COMB298Y": &dto.COMB298YReq{},
|
||||
"COMB86PM": &dto.COMB86PMReq{},
|
||||
"QCXG7A2B": &dto.QCXG7A2BReq{},
|
||||
"COMENT01": &dto.COMENT01Req{},
|
||||
"JRZQ09J8": &dto.JRZQ09J8Req{},
|
||||
"FLXGDEA8": &dto.FLXGDEA8Req{},
|
||||
"FLXGDEA9": &dto.FLXGDEA9Req{},
|
||||
"JRZQ1D09": &dto.JRZQ1D09Req{},
|
||||
"IVYZ2A8B": &dto.IVYZ2A8BReq{},
|
||||
"IVYZ7C9D": &dto.IVYZ7C9DReq{},
|
||||
"IVYZ5E3F": &dto.IVYZ5E3FReq{},
|
||||
"YYSY4F2E": &dto.YYSY4F2EReq{},
|
||||
"YYSY8B1C": &dto.YYSY8B1CReq{},
|
||||
"YYSY6D9A": &dto.YYSY6D9AReq{},
|
||||
"YYSY3E7F": &dto.YYSY3E7FReq{},
|
||||
"FLXG5A3B": &dto.FLXG5A3BReq{},
|
||||
"FLXG9C1D": &dto.FLXG9C1DReq{},
|
||||
"FLXG2E8F": &dto.FLXG2E8FReq{},
|
||||
"JRZQ3C7B": &dto.JRZQ3C7BReq{},
|
||||
"JRZQ8A2D": &dto.JRZQ8A2DReq{},
|
||||
"JRZQ5E9F": &dto.JRZQ5E9FReq{},
|
||||
"JRZQ4B6C": &dto.JRZQ4B6CReq{},
|
||||
"JRZQ7F1A": &dto.JRZQ7F1AReq{},
|
||||
"DWBG6A2C": &dto.DWBG6A2CReq{},
|
||||
"DWBG8B4D": &dto.DWBG8B4DReq{},
|
||||
"FLXG8B4D": &dto.FLXG8B4DReq{},
|
||||
"IVYZ81NC": &dto.IVYZ81NCReq{},
|
||||
"IVYZ7F3A": &dto.IVYZ7F3AReq{},
|
||||
"IVYZ3P9M": &dto.IVYZ3P9MReq{},
|
||||
"IVYZ3A7F": &dto.IVYZ3A7FReq{},
|
||||
"IVYZ9D2E": &dto.IVYZ9D2EReq{},
|
||||
"IVYZ9K2L": &dto.IVYZ9K2LReq{},
|
||||
"DWBG7F3A": &dto.DWBG7F3AReq{},
|
||||
"YYSY8F3A": &dto.YYSY8F3AReq{},
|
||||
"QCXG9P1C": &dto.QCXG9P1CReq{},
|
||||
"JRZQ9E2A": &dto.JRZQ9E2AReq{},
|
||||
"YYSY9A1B": &dto.YYSY9A1BReq{},
|
||||
"YYSY8C2D": &dto.YYSY8C2DReq{},
|
||||
"YYSY7D3E": &dto.YYSY7D3EReq{},
|
||||
"YYSY9E4A": &dto.YYSY9E4AReq{},
|
||||
"JRZQ6F2A": &dto.JRZQ6F2AReq{},
|
||||
"JRZQ8B3C": &dto.JRZQ8B3CReq{},
|
||||
"JRZQ9D4E": &dto.JRZQ9D4EReq{},
|
||||
"FLXG7E8F": &dto.FLXG7E8FReq{},
|
||||
"QYGL5F6A": &dto.QYGL5F6AReq{},
|
||||
"IVYZ6G7H": &dto.IVYZ6G7HReq{},
|
||||
"IVYZ8I9J": &dto.IVYZ8I9JReq{},
|
||||
"JRZQ0L85": &dto.JRZQ0L85Req{},
|
||||
"COMBHZY2": &dto.COMBHZY2Req{}, //
|
||||
"QCXG8A3D": &dto.QCXG8A3DReq{},
|
||||
"QCXG6B4E": &dto.QCXG6B4EReq{},
|
||||
"QYGL2B5C": &dto.QYGL2B5CReq{},
|
||||
"JRZQ2F8A": &dto.JRZQ2F8AReq{},
|
||||
"JRZQ1E7B": &dto.JRZQ1E7BReq{},
|
||||
"JRZQ3C9R": &dto.JRZQ3C9RReq{},
|
||||
"IVYZ2C1P": &dto.IVYZ2C1PReq{},
|
||||
"YYSY9F1B": &dto.YYSY9F1BReq{},
|
||||
"YYSY6F2B": &dto.YYSY6F2BReq{},
|
||||
"QYGL6S1B": &dto.QYGL6S1BReq{},
|
||||
"JRZQ0B6Y": &dto.JRZQ0B6YReq{},
|
||||
"JRZQ9A1W": &dto.JRZQ9A1WReq{},
|
||||
"JRZQ8F7C": &dto.JRZQ8F7CReq{}, //综合多头
|
||||
"FLXGK5D2": &dto.FLXGK5D2Req{},
|
||||
"FLXG3A9B": &dto.FLXG3A9BReq{},
|
||||
"IVYZP2Q6": &dto.IVYZP2Q6Req{},
|
||||
"JRZQ1W4X": &dto.JRZQ1W4XReq{}, //全景档案
|
||||
"QYGL2S0W": &dto.QYGL2S0WReq{}, //失信被执行企业个人查询
|
||||
"QYGL9T1Q": &dto.QYGL9T1QReq{}, //全国企业借贷意向验证查询_V1
|
||||
"QYGL5A9T": &dto.QYGL5A9TReq{}, //全国企业各类工商风险统计数量查询
|
||||
"JRZQ3P01": &dto.JRZQ3P01Req{}, //天远风控决策
|
||||
"JRZQ3AG6": &dto.JRZQ3AG6Req{}, //轻松查公积
|
||||
"IVYZ2B2T": &dto.IVYZ2B2TReq{}, //能力资质核验(学历)
|
||||
"IVYZ5A9O": &dto.IVYZ5A9OReq{}, //全国⾃然⼈⻛险评估评分模型
|
||||
"IVYZ6M8P": &dto.IVYZ6M8PReq{}, //职业资格证书
|
||||
"IVYZ9H2M": &dto.IVYZ9H2MReq{}, //极光个人婚姻查询(V2版)
|
||||
"QYGL5CMP": &dto.QYGL5CMPReq{}, //企业五要素验证
|
||||
"QCXG4896": &dto.QCXG4896Req{}, //网约车风险查询
|
||||
"IVYZZQT3": &dto.IVYZZQT3Req{}, //人脸比对V3
|
||||
"IVYZBPQ2": &dto.IVYZBPQ2Req{}, //人脸比对V2
|
||||
"IVYZSFEL": &dto.IVYZSFELReq{}, //全国自然人人像三要素核验_V1
|
||||
"QYGL66SL": &dto.QYGL66SLReq{}, //全国企业司法模型服务查询_V1
|
||||
"QCXG5F3A": &dto.QCXG5F3AReq{}, //极光个人车辆查询
|
||||
"QCXG4D2E": &dto.QCXG4D2EReq{}, //极光名下车辆数量查询
|
||||
"QYGLP0HT": &dto.QYGLP0HTReq{}, //股权穿透
|
||||
"QYGL2NAO": &dto.QYGL2naoReq{}, //股权变更
|
||||
"QYGLNIO8": &dto.QYGLNIO8Req{}, //企业基本信息
|
||||
"QYGL4B2E": &dto.QYGL5A3CReq{}, //税收违法
|
||||
"QYGL7D9A": &dto.QYGL5A3CReq{}, //欠税公告
|
||||
"IVYZ0S0D": &dto.IVYZ0S0DReq{}, //劳动仲裁信息查询(个人版)
|
||||
"IVYZ1J7H": &dto.IVYZ1J7HReq{}, //行驶证核查v2
|
||||
"QCXGJJ2A": &dto.QCXGJJ2AReq{}, //vin码查车辆信息(一对多)
|
||||
"QCXGGJ3A": &dto.QCXGGJ3AReq{}, //车辆vin码查询号牌
|
||||
"QCXGYTS2": &dto.QCXGYTS2Req{}, //车辆二要素核验v2
|
||||
"QCXGP00W": &dto.QCXGP00WReq{}, //车辆出险详版查询
|
||||
"QCXGGB2Q": &dto.QCXGGB2QReq{}, //车辆二要素核验V1
|
||||
"QCXG4I1Z": &dto.QCXG4I1ZReq{}, //车辆过户详版查询
|
||||
"QCXG1H7Y": &dto.QCXG1H7YReq{}, //车辆过户简版查询
|
||||
"QCXG3Z3L": &dto.QCXG3Z3LReq{}, //车辆维保详细版查询
|
||||
"QCXG3Y6B": &dto.QCXG1U4UReq{}, //车辆维保简版查询
|
||||
"QCXG2T6S": &dto.QCXG2T6SReq{}, //车辆里程记录(品牌查询)
|
||||
"QCXG1U4U": &dto.QCXG1U4UReq{}, //车辆里程记录(混合查询)
|
||||
"JRZQO6L7": &dto.JRZQO6L7Req{}, //全国自然人经济特征评分模型v3 简版
|
||||
"JRZQO7L1": &dto.JRZQO7L1Req{}, //全国自然人经济特征评分模型v4 详版
|
||||
"JRZQS7G0": &dto.JRZQS7G0Req{}, //社保综合评分V1
|
||||
"IVYZ9K7F": &dto.IVYZ9K7FReq{}, //身份证实名认证即时版
|
||||
"YYSY3M8S": &dto.YYSY3M8SReq{}, //运营商二要素查询
|
||||
"YYSYC4R9": &dto.YYSYC4R9Req{}, //运营商三要素详版查询
|
||||
"YYSYH6D2": &dto.YYSYH6D2Req{}, //运营商三要素简版政务版查询
|
||||
"YYSYP0T4": &dto.YYSYP0T4Req{}, //在网时长查询
|
||||
"YYSYE7V5": &dto.YYSYE7V5Req{}, //手机在网状态查询
|
||||
"YYSYS9W1": &dto.YYSYS9W1Req{}, //手机携号转网查询
|
||||
"YYSYK8R3": &dto.YYSYK8R3Req{}, //手机空号检测查询
|
||||
"YYSYF2T7": &dto.YYSYF2T7Req{}, //手机二次放号检测查询
|
||||
"IVYZA1B3": &dto.IVYZA1B3Req{}, //公安三要素人脸识别
|
||||
"IVYZX5QZ": &dto.IVYZX5QZReq{}, //活体识别
|
||||
"IVYZN2P8": &dto.IVYZ9K7FReq{}, //身份证实名认证政务版
|
||||
"YYSYH6F3": &dto.YYSYH6F3Req{}, //运营商三要素简版即时版查询
|
||||
"IVYZX5Q2": &dto.IVYZX5Q2Req{}, //活体识别步骤二
|
||||
"PDFG01GZ": &dto.PDFG01GZReq{}, //
|
||||
"QYGL5S1I": &dto.QYGL5S1IReq{}, //企业司法涉诉V2
|
||||
"JRZQACAB": &dto.JRZQACABReq{}, //银行卡四要素
|
||||
"QCXG9F5C": &dto.QCXG9F5CReq{}, //疑似营运车辆注册平台数 10386
|
||||
"QCXG3B8Z": &dto.QCXG3B8ZReq{}, //疑似运营车辆查询(月度里程)10268
|
||||
"QCXGP1W3": &dto.QCXGP1W3Req{}, //疑似运营车辆查询(季度里程)10269
|
||||
"QCXGM7R9": &dto.QCXGM7R9Req{}, //疑似运营车辆查询(半年度里程)10270
|
||||
"QCXGU2K4": &dto.QCXGU2K4Req{}, //疑似运营车辆查询(年度里程)10271
|
||||
"QCXG5U0Z": &dto.QCXG5U0ZReq{}, //车辆静态信息查询 10479
|
||||
"QCXGY7F2": &dto.QCXGY7F2Req{}, //二手车VIN估值 10443
|
||||
"YYSYK9R4": &dto.YYSYK9R4Req{}, //全网手机三要素验证1979周更新版
|
||||
"QCXG3M7Z": &dto.QCXG3M7ZReq{}, //人车关系核验(ETC)10093 月更
|
||||
"JRZQ1P5G": &dto.JRZQ1P5GReq{}, //全国自然人借贷压力指数查询(2)
|
||||
"IVYZOCR1": &dto.IVYZOCR1Req{}, //身份证OCR
|
||||
"IVYZOCR2": &dto.IVYZOCR1Req{}, //身份证OCR2数卖
|
||||
"QYGLJ0Q1": &dto.QYGLJ0Q1Req{}, //企业股权结构全景查询
|
||||
"QYGLUY3S": &dto.QYGLUY3SReq{}, //企业全量信息核验V2 可用
|
||||
"JRZQOCRE": &dto.JRZQOCREReq{}, //银行卡OCR数卖
|
||||
"JRZQOCRY": &dto.JRZQOCRYReq{}, //银行卡OCR数据宝
|
||||
"YYSY35TA": &dto.YYSY35TAReq{}, //运营商归属地数卖
|
||||
"IVYZ9363": &dto.IVYZ9363Req{},
|
||||
"IVYZ385E": &dto.IVYZ385EReq{},
|
||||
"IVYZ5733": &dto.IVYZ5733Req{},
|
||||
"FLXG3D56": &dto.FLXG3D56Req{},
|
||||
"FLXG75FE": &dto.FLXG75FEReq{},
|
||||
"FLXG0V3B": &dto.FLXG0V3BReq{},
|
||||
"FLXG0V4B": &dto.FLXG0V4BReq{},
|
||||
"FLXG54F5": &dto.FLXG54F5Req{},
|
||||
"FLXG162A": &dto.FLXG162AReq{},
|
||||
"FLXG0687": &dto.FLXG0687Req{},
|
||||
"FLXGBC21": &dto.FLXGBC21Req{},
|
||||
"FLXG970F": &dto.FLXG970FReq{},
|
||||
"FLXG5876": &dto.FLXG5876Req{},
|
||||
"FLXG9687": &dto.FLXG9687Req{},
|
||||
"FLXGC9D1": &dto.FLXGC9D1Req{},
|
||||
"FLXGCA3D": &dto.FLXGCA3DReq{},
|
||||
"FLXGDEC7": &dto.FLXGDEC7Req{},
|
||||
"JRZQ0A03": &dto.JRZQ0A03Req{},
|
||||
"JRZQ4AA8": &dto.JRZQ4AA8Req{},
|
||||
"JRZQ8203": &dto.JRZQ8203Req{},
|
||||
"JRZQDCBE": &dto.JRZQDCBEReq{},
|
||||
"QYGL2ACD": &dto.QYGL2ACDReq{},
|
||||
"QYGL6F2D": &dto.QYGL6F2DReq{},
|
||||
"QYGL45BD": &dto.QYGL45BDReq{},
|
||||
"QYGL8261": &dto.QYGL8261Req{},
|
||||
"QYGL8271": &dto.QYGL8271Req{},
|
||||
"QYGLB4C0": &dto.QYGLB4C0Req{},
|
||||
"QYGL23T7": &dto.QYGL23T7Req{},
|
||||
"QYGL5A3C": &dto.QYGL5A3CReq{},
|
||||
"QYGL8B4D": &dto.QYGL8B4DReq{},
|
||||
"QYGL9E2F": &dto.QYGL9E2FReq{},
|
||||
"QYGL7C1A": &dto.QYGL7C1AReq{},
|
||||
"QYGL3F8E": &dto.QYGL3F8EReq{},
|
||||
"YYSY4B37": &dto.YYSY4B37Req{},
|
||||
"YYSY4B21": &dto.YYSY4B21Req{},
|
||||
"YYSY6F2E": &dto.YYSY6F2EReq{},
|
||||
"YYSY09CD": &dto.YYSY09CDReq{},
|
||||
"IVYZ0B03": &dto.IVYZ0B03Req{},
|
||||
"YYSYBE08": &dto.YYSYBE08Req{},
|
||||
"YYSYBE08TEST": &dto.YYSYBE08Req{},
|
||||
"YYSYD50F": &dto.YYSYD50FReq{},
|
||||
"YYSYF7DB": &dto.YYSYF7DBReq{},
|
||||
"IVYZ9A2B": &dto.IVYZ9A2BReq{},
|
||||
"IVYZ7F2A": &dto.IVYZ7F2AReq{},
|
||||
"IVYZ4E8B": &dto.IVYZ4E8BReq{},
|
||||
"IVYZ1C9D": &dto.IVYZ1C9DReq{},
|
||||
"IVYZGZ08": &dto.IVYZGZ08Req{},
|
||||
"FLXG8A3F": &dto.FLXG8A3FReq{},
|
||||
"FLXG5B2E": &dto.FLXG5B2EReq{},
|
||||
"COMB298Y": &dto.COMB298YReq{},
|
||||
"COMB86PM": &dto.COMB86PMReq{},
|
||||
"QCXG7A2B": &dto.QCXG7A2BReq{},
|
||||
"COMENT01": &dto.COMENT01Req{},
|
||||
"JRZQ09J8": &dto.JRZQ09J8Req{},
|
||||
"FLXGDEA8": &dto.FLXGDEA8Req{},
|
||||
"FLXGDEA9": &dto.FLXGDEA9Req{},
|
||||
"JRZQ1D09": &dto.JRZQ1D09Req{},
|
||||
"IVYZ2A8B": &dto.IVYZ2A8BReq{},
|
||||
"IVYZ7C9D": &dto.IVYZ7C9DReq{},
|
||||
"IVYZ5E3F": &dto.IVYZ5E3FReq{},
|
||||
"YYSY4F2E": &dto.YYSY4F2EReq{},
|
||||
"YYSY8B1C": &dto.YYSY8B1CReq{},
|
||||
"YYSY6D9A": &dto.YYSY6D9AReq{},
|
||||
"YYSY3E7F": &dto.YYSY3E7FReq{},
|
||||
"FLXG5A3B": &dto.FLXG5A3BReq{},
|
||||
"FLXG9C1D": &dto.FLXG9C1DReq{},
|
||||
"FLXG2E8F": &dto.FLXG2E8FReq{},
|
||||
"JRZQ3C7B": &dto.JRZQ3C7BReq{},
|
||||
"JRZQ8A2D": &dto.JRZQ8A2DReq{},
|
||||
"JRZQ5E9F": &dto.JRZQ5E9FReq{},
|
||||
"JRZQ4B6C": &dto.JRZQ4B6CReq{},
|
||||
"JRZQ7F1A": &dto.JRZQ7F1AReq{},
|
||||
"DWBG6A2C": &dto.DWBG6A2CReq{},
|
||||
"DWBG8B4D": &dto.DWBG8B4DReq{},
|
||||
"FLXG8B4D": &dto.FLXG8B4DReq{},
|
||||
"IVYZ81NC": &dto.IVYZ81NCReq{},
|
||||
"IVYZ2MN6": &dto.IVYZ2MN6Req{},
|
||||
"IVYZ7F3A": &dto.IVYZ7F3AReq{},
|
||||
"IVYZ3P9M": &dto.IVYZ3P9MReq{},
|
||||
"IVYZ3A7F": &dto.IVYZ3A7FReq{},
|
||||
"IVYZ9D2E": &dto.IVYZ9D2EReq{},
|
||||
"IVYZ9K2L": &dto.IVYZ9K2LReq{},
|
||||
"DWBG7F3A": &dto.DWBG7F3AReq{},
|
||||
"YYSY8F3A": &dto.YYSY8F3AReq{},
|
||||
"QCXG9P1C": &dto.QCXG9P1CReq{},
|
||||
"JRZQ9E2A": &dto.JRZQ9E2AReq{},
|
||||
"YYSY9A1B": &dto.YYSY9A1BReq{},
|
||||
"YYSY8C2D": &dto.YYSY8C2DReq{},
|
||||
"YYSY7D3E": &dto.YYSY7D3EReq{},
|
||||
"YYSY9E4A": &dto.YYSY9E4AReq{},
|
||||
"JRZQ6F2A": &dto.JRZQ6F2AReq{},
|
||||
"JRZQ8B3C": &dto.JRZQ8B3CReq{},
|
||||
"JRZQ9D4E": &dto.JRZQ9D4EReq{},
|
||||
"FLXG7E8F": &dto.FLXG7E8FReq{},
|
||||
"QYGL5F6A": &dto.QYGL5F6AReq{},
|
||||
"IVYZ6G7H": &dto.IVYZ6G7HReq{},
|
||||
"IVYZ8I9J": &dto.IVYZ8I9JReq{},
|
||||
"JRZQ0L85": &dto.JRZQ0L85Req{},
|
||||
"COMBHZY2": &dto.COMBHZY2Req{}, //
|
||||
"QCXG8A3D": &dto.QCXG8A3DReq{},
|
||||
"QCXG6B4E": &dto.QCXG6B4EReq{},
|
||||
"QYGL2B5C": &dto.QYGL2B5CReq{},
|
||||
"QYGLJ1U9": &dto.QYGLJ1U9Req{},
|
||||
"JRZQ2F8A": &dto.JRZQ2F8AReq{},
|
||||
"JRZQ1E7B": &dto.JRZQ1E7BReq{},
|
||||
"JRZQ3C9R": &dto.JRZQ3C9RReq{},
|
||||
"IVYZ2C1P": &dto.IVYZ2C1PReq{},
|
||||
"YYSY9F1B": &dto.YYSY9F1BReq{},
|
||||
"YYSY6F2B": &dto.YYSY6F2BReq{},
|
||||
"QYGL6S1B": &dto.QYGL6S1BReq{},
|
||||
"JRZQ0B6Y": &dto.JRZQ0B6YReq{},
|
||||
"JRZQ9A1W": &dto.JRZQ9A1WReq{},
|
||||
"JRZQ8F7C": &dto.JRZQ8F7CReq{}, //综合多头
|
||||
"FLXGK5D2": &dto.FLXGK5D2Req{},
|
||||
"FLXG3A9B": &dto.FLXG3A9BReq{},
|
||||
"IVYZP2Q6": &dto.IVYZP2Q6Req{},
|
||||
"JRZQ1W4X": &dto.JRZQ1W4XReq{}, //全景档案
|
||||
"QYGL2S0W": &dto.QYGL2S0WReq{}, //失信被执行企业个人查询
|
||||
"QYGL9T1Q": &dto.QYGL9T1QReq{}, //全国企业借贷意向验证查询_V1
|
||||
"QYGL5A9T": &dto.QYGL5A9TReq{}, //全国企业各类工商风险统计数量查询
|
||||
"JRZQ3P01": &dto.JRZQ3P01Req{}, //天远风控决策
|
||||
"JRZQ3AG6": &dto.JRZQ3AG6Req{}, //轻松查公积
|
||||
"IVYZ2B2T": &dto.IVYZ2B2TReq{}, //能力资质核验(学历)
|
||||
"IVYZ5A9O": &dto.IVYZ5A9OReq{}, //全国⾃然⼈⻛险评估评分模型
|
||||
"IVYZ6M8P": &dto.IVYZ6M8PReq{}, //职业资格证书
|
||||
"IVYZ9H2M": &dto.IVYZ9H2MReq{}, //极光个人婚姻查询(V2版)
|
||||
"QYGL5CMP": &dto.QYGL5CMPReq{}, //企业五要素验证
|
||||
"QCXG4896": &dto.QCXG4896Req{}, //网约车风险查询
|
||||
"IVYZZQT3": &dto.IVYZZQT3Req{}, //人脸比对V3
|
||||
"IVYZBPQ2": &dto.IVYZBPQ2Req{}, //人脸比对V2
|
||||
"IVYZSFEL": &dto.IVYZSFELReq{}, //全国自然人人像三要素核验_V1
|
||||
"QYGL66SL": &dto.QYGL66SLReq{}, //全国企业司法模型服务查询_V1
|
||||
"QCXG5F3A": &dto.QCXG5F3AReq{}, //极光个人车辆查询
|
||||
"QCXG4D2E": &dto.QCXG4D2EReq{}, //极光名下车辆数量查询
|
||||
"QYGLP0HT": &dto.QYGLP0HTReq{}, //股权穿透
|
||||
"QYGL2NAO": &dto.QYGL2naoReq{}, //股权变更
|
||||
"QYGLNIO8": &dto.QYGLNIO8Req{}, //企业基本信息
|
||||
"QYGL4B2E": &dto.QYGL5A3CReq{}, //税收违法
|
||||
"QYGL7D9A": &dto.QYGL5A3CReq{}, //欠税公告
|
||||
"IVYZ0S0D": &dto.IVYZ0S0DReq{}, //劳动仲裁信息查询(个人版)
|
||||
"IVYZ1J7H": &dto.IVYZ1J7HReq{}, //行驶证核查v2
|
||||
"QCXGJJ2A": &dto.QCXGJJ2AReq{}, //vin码查车辆信息(一对多)
|
||||
"QCXGGJ3A": &dto.QCXGGJ3AReq{}, //车辆vin码查询号牌
|
||||
"QCXGYTS2": &dto.QCXGYTS2Req{}, //车辆二要素核验v2
|
||||
"QCXGP00W": &dto.QCXGP00WReq{}, //车辆出险详版查询
|
||||
"QCXGGB2Q": &dto.QCXGGB2QReq{}, //车辆二要素核验V1
|
||||
"QCXG4I1Z": &dto.QCXG4I1ZReq{}, //车辆过户详版查询
|
||||
"QCXG1H7Y": &dto.QCXG1H7YReq{}, //车辆过户简版查询
|
||||
"QCXG3Z3L": &dto.QCXG3Z3LReq{}, //车辆维保详细版查询
|
||||
"QCXG3Y6B": &dto.QCXG1U4UReq{}, //车辆维保简版查询
|
||||
"QCXG2T6S": &dto.QCXG2T6SReq{}, //车辆里程记录(品牌查询)
|
||||
"QCXG1U4U": &dto.QCXG1U4UReq{}, //车辆里程记录(混合查询)
|
||||
"JRZQO6L7": &dto.JRZQO6L7Req{}, //全国自然人经济特征评分模型v3 简版
|
||||
"JRZQO7L1": &dto.JRZQO7L1Req{}, //全国自然人经济特征评分模型v4 详版
|
||||
"JRZQS7G0": &dto.JRZQS7G0Req{}, //社保综合评分V1
|
||||
"IVYZ9K7F": &dto.IVYZ9K7FReq{}, //身份证实名认证即时版
|
||||
"YYSY3M8S": &dto.YYSY3M8SReq{}, //运营商二要素查询
|
||||
"YYSYC4R9": &dto.YYSYC4R9Req{}, //运营商三要素详版查询
|
||||
"YYSYH6D2": &dto.YYSYH6D2Req{}, //运营商三要素简版政务版查询
|
||||
"YYSYP0T4": &dto.YYSYP0T4Req{}, //在网时长查询
|
||||
"YYSYE7V5": &dto.YYSYE7V5Req{}, //手机在网状态查询
|
||||
"YYSYS9W1": &dto.YYSYS9W1Req{}, //手机携号转网查询
|
||||
"YYSYK8R3": &dto.YYSYK8R3Req{}, //手机空号检测查询
|
||||
"YYSYF2T7": &dto.YYSYF2T7Req{}, //手机二次放号检测查询
|
||||
"IVYZA1B3": &dto.IVYZA1B3Req{}, //公安三要素人脸识别
|
||||
"IVYZFIC1": &dto.IVYZFIC1Req{}, //人脸身份证比对(数脉)
|
||||
"IVYZX5QZ": &dto.IVYZX5QZReq{}, //活体识别
|
||||
"IVYZN2P8": &dto.IVYZ9K7FReq{}, //身份证实名认证政务版
|
||||
"YYSYH6F3": &dto.YYSYH6F3Req{}, //运营商三要素简版即时版查询
|
||||
"IVYZX5Q2": &dto.IVYZX5Q2Req{}, //活体识别步骤二
|
||||
"PDFG01GZ": &dto.PDFG01GZReq{}, //
|
||||
"QYGL5S1I": &dto.QYGL5S1IReq{}, //企业司法涉诉V2
|
||||
"JRZQACAB": &dto.JRZQACABReq{}, //银行卡四要素
|
||||
"QCXG9F5C": &dto.QCXG9F5CReq{}, //疑似营运车辆注册平台数 10386
|
||||
"QCXG3B8Z": &dto.QCXG3B8ZReq{}, //疑似运营车辆查询(月度里程)10268
|
||||
"QCXGP1W3": &dto.QCXGP1W3Req{}, //疑似运营车辆查询(季度里程)10269
|
||||
"QCXGM7R9": &dto.QCXGM7R9Req{}, //疑似运营车辆查询(半年度里程)10270
|
||||
"QCXGU2K4": &dto.QCXGU2K4Req{}, //疑似运营车辆查询(年度里程)10271
|
||||
"QCXG5U0Z": &dto.QCXG5U0ZReq{}, //车辆静态信息查询 10479
|
||||
"QCXGY7F2": &dto.QCXGY7F2Req{}, //二手车VIN估值 10443
|
||||
"YYSYK9R4": &dto.YYSYK9R4Req{}, //全网手机三要素验证1979周更新版
|
||||
"QCXG3M7Z": &dto.QCXG3M7ZReq{}, //人车关系核验(ETC)10093 月更
|
||||
"JRZQ1P5G": &dto.JRZQ1P5GReq{}, //全国自然人借贷压力指数查询(2)
|
||||
"IVYZOCR1": &dto.IVYZOCR1Req{}, //身份证OCR
|
||||
"IVYZOCR2": &dto.IVYZOCR1Req{}, //身份证OCR2数卖
|
||||
"QYGLJ0Q1": &dto.QYGLJ0Q1Req{}, //企业股权结构全景查询
|
||||
"QYGLUY3S": &dto.QYGLUY3SReq{}, //企业全量信息核验V2 可用
|
||||
"JRZQOCRE": &dto.JRZQOCREReq{}, //银行卡OCR数卖
|
||||
"JRZQOCRY": &dto.JRZQOCRYReq{}, //银行卡OCR数据宝
|
||||
"YYSY35TA": &dto.YYSY35TAReq{}, //运营商归属地数卖
|
||||
"QYGLDJ12": &dto.QYGLDJ12Req{}, //企业年报信息核验
|
||||
"FLXGDJG3": &dto.FLXGDJG3Req{}, //董监高司法综合信息核验
|
||||
"QYGL8848": &dto.QYGLDJ12Req{}, //企业税收违法核查
|
||||
"IVYZ18HY": &dto.IVYZ18HYReq{}, //婚姻状况核验V2(单人)
|
||||
"IVYZ28HY": &dto.IVYZ28HYReq{}, //婚姻状况核验(单人)
|
||||
"IVYZ38SR": &dto.IVYZ38SRReq{}, //婚姻状态核验(双人)
|
||||
"IVYZ48SR": &dto.IVYZ48SRReq{}, //婚姻状态核验V2(双人)
|
||||
"IVYZ5E22": &dto.IVYZ5E22Req{}, //双人婚姻评估查询zhicha版本
|
||||
"DWBG5SAM": &dto.DWBG5SAMReq{}, //天远指迷报告
|
||||
"QYGLDJ33": &dto.QYGLDJ33Req{}, //企业年报信息核验
|
||||
"IVYZRAX1": &dto.IVYZRAX1Req{},//融安信用分
|
||||
"IVYZRAX2": &dto.IVYZRAX1Req{},//融御反欺诈
|
||||
}
|
||||
|
||||
// 优先返回已配置的DTO
|
||||
@@ -383,6 +399,8 @@ func (s *FormConfigServiceImpl) parseValidationRules(validateTag string) string
|
||||
frontendRules = append(frontendRules, "授权链接格式")
|
||||
case rule == "validBase64Image":
|
||||
frontendRules = append(frontendRules, "Base64图片格式(JPG、BMP、PNG)")
|
||||
case rule == "base64" || rule == "validBase64":
|
||||
frontendRules = append(frontendRules, "Base64编码格式(支持图片/PDF)")
|
||||
case strings.HasPrefix(rule, "oneof="):
|
||||
values := strings.TrimPrefix(rule, "oneof=")
|
||||
frontendRules = append(frontendRules, "可选值: "+values)
|
||||
@@ -416,7 +434,7 @@ func (s *FormConfigServiceImpl) getFieldType(fieldType reflect.Type, validation
|
||||
return "url"
|
||||
} else if strings.Contains(validation, "可选值") {
|
||||
return "select"
|
||||
} else if strings.Contains(validation, "Base64图片") || strings.Contains(validation, "base64") {
|
||||
} else if strings.Contains(validation, "Base64图片") || strings.Contains(validation, "Base64编码") || strings.Contains(validation, "base64") {
|
||||
return "textarea"
|
||||
} else if strings.Contains(validation, "图片地址") {
|
||||
return "url"
|
||||
@@ -435,58 +453,61 @@ func (s *FormConfigServiceImpl) getFieldType(fieldType reflect.Type, validation
|
||||
func (s *FormConfigServiceImpl) generateFieldLabel(jsonTag string) string {
|
||||
// 将下划线命名转换为中文标签
|
||||
labelMap := map[string]string{
|
||||
"mobile_no": "手机号码",
|
||||
"id_card": "身份证号",
|
||||
"name": "姓名",
|
||||
"man_name": "男方姓名",
|
||||
"woman_name": "女方姓名",
|
||||
"man_id_card": "男方身份证",
|
||||
"woman_id_card": "女方身份证",
|
||||
"ent_name": "企业名称",
|
||||
"legal_person": "法人姓名",
|
||||
"ent_code": "企业代码",
|
||||
"ent_reg_no": "企业注册号",
|
||||
"auth_date": "授权日期",
|
||||
"date_range": "日期范围",
|
||||
"time_range": "时间范围",
|
||||
"authorized": "是否授权",
|
||||
"authorization_url": "授权链接",
|
||||
"unique_id": "唯一标识",
|
||||
"return_url": "返回链接",
|
||||
"mobile_type": "手机类型",
|
||||
"start_date": "开始日期",
|
||||
"years": "年数",
|
||||
"bank_card": "银行卡号",
|
||||
"user_type": "关系类型",
|
||||
"vehicle_type": "车辆类型",
|
||||
"page_num": "页码",
|
||||
"page_size": "每页数量",
|
||||
"use_scenario": "使用场景",
|
||||
"auth_authorize_file_code": "授权文件编码",
|
||||
"plate_no": "车牌号",
|
||||
"plate_type": "号牌类型",
|
||||
"vin_code": "车辆识别代号VIN码",
|
||||
"return_type": "返回类型",
|
||||
"photo_data": "入参图片base64编码",
|
||||
"owner_type": "企业主类型",
|
||||
"type": "查询类型",
|
||||
"query_reason_id": "查询原因ID",
|
||||
"flag": "层次",
|
||||
"dir": "方向",
|
||||
"min_percent": "股权穿透比例下限",
|
||||
"max_percent": "股权穿透比例上限",
|
||||
"engine_number": "发动机号码",
|
||||
"notice_model": "车辆型号",
|
||||
"vlphoto_data": "行驶证图片",
|
||||
"carplate_type": "车辆号牌类型",
|
||||
"image_url": "入参图片地址",
|
||||
"reg_url": "车辆登记证图片地址",
|
||||
"token": "token采集及获取结果时所使用的凭证,有效期2个小时,在此时效内,应用侧可以发起采集请求(重复的采集所触发的结果会被忽略)和结果查询",
|
||||
"vehicle_name": "车型名称",
|
||||
"vehicle_location": "车辆所在地",
|
||||
"first_registrationdate": "首次登记日期",
|
||||
"color": "颜色",
|
||||
"plate_color": "车牌颜色",
|
||||
"mobile_no": "手机号码",
|
||||
"id_card": "身份证号",
|
||||
"idCard": "身份证号",
|
||||
"name": "姓名",
|
||||
"man_name": "男方姓名",
|
||||
"woman_name": "女方姓名",
|
||||
"man_id_card": "男方身份证",
|
||||
"woman_id_card": "女方身份证",
|
||||
"ent_name": "企业名称",
|
||||
"legal_person": "法人姓名",
|
||||
"ent_code": "企业代码",
|
||||
"ent_reg_no": "企业注册号",
|
||||
"auth_date": "授权日期",
|
||||
"date_range": "日期范围",
|
||||
"time_range": "时间范围",
|
||||
"authorized": "是否授权",
|
||||
"authorization_url": "授权链接",
|
||||
"unique_id": "唯一标识",
|
||||
"return_url": "返回链接",
|
||||
"mobile_type": "手机类型",
|
||||
"start_date": "开始日期",
|
||||
"years": "年数",
|
||||
"bank_card": "银行卡号",
|
||||
"user_type": "关系类型",
|
||||
"vehicle_type": "车辆类型",
|
||||
"page_num": "页码",
|
||||
"page_size": "每页数量",
|
||||
"use_scenario": "使用场景",
|
||||
"auth_authorize_file_code": "授权文件编码",
|
||||
"plate_no": "车牌号",
|
||||
"plate_type": "号牌类型",
|
||||
"vin_code": "车辆识别代号VIN码",
|
||||
"return_type": "返回类型",
|
||||
"photo_data": "入参图片base64编码",
|
||||
"owner_type": "企业主类型",
|
||||
"type": "查询类型",
|
||||
"query_reason_id": "查询原因ID",
|
||||
"flag": "层次",
|
||||
"dir": "方向",
|
||||
"min_percent": "股权穿透比例下限",
|
||||
"max_percent": "股权穿透比例上限",
|
||||
"engine_number": "发动机号码",
|
||||
"notice_model": "车辆型号",
|
||||
"vlphoto_data": "行驶证图片",
|
||||
"carplate_type": "车辆号牌类型",
|
||||
"image_url": "入参图片地址",
|
||||
"reg_url": "车辆登记证图片地址",
|
||||
"token": "token采集及获取结果时所使用的凭证,有效期2个小时,在此时效内,应用侧可以发起采集请求(重复的采集所触发的结果会被忽略)和结果查询",
|
||||
"vehicle_name": "车型名称",
|
||||
"vehicle_location": "车辆所在地",
|
||||
"first_registrationdate": "首次登记日期",
|
||||
"color": "颜色",
|
||||
"plate_color": "车牌颜色",
|
||||
"marital_type": "婚姻状况类型",
|
||||
"auth_authorize_file_base64": "PDF授权文件Base64编码(5MB以内)",
|
||||
}
|
||||
|
||||
if label, exists := labelMap[jsonTag]; exists {
|
||||
@@ -500,56 +521,59 @@ func (s *FormConfigServiceImpl) generateFieldLabel(jsonTag string) string {
|
||||
// generateExampleValue 生成示例值
|
||||
func (s *FormConfigServiceImpl) generateExampleValue(fieldType reflect.Type, jsonTag string) string {
|
||||
exampleMap := map[string]string{
|
||||
"mobile_no": "13800138000",
|
||||
"id_card": "110101199001011234",
|
||||
"name": "张三",
|
||||
"man_name": "张三",
|
||||
"woman_name": "李四",
|
||||
"ent_name": "示例企业有限公司",
|
||||
"legal_person": "王五",
|
||||
"ent_code": "91110000123456789X",
|
||||
"ent_reg_no": "110000000123456",
|
||||
"auth_date": "20240101-20241231",
|
||||
"date_range": "20240101-20241231",
|
||||
"time_range": "09:00-18:00",
|
||||
"authorized": "1",
|
||||
"years": "5",
|
||||
"bank_card": "6222021234567890123",
|
||||
"mobile_type": "移动",
|
||||
"start_date": "2024-01-01",
|
||||
"unique_id": "UNIQUE123456",
|
||||
"return_url": "https://example.com/return",
|
||||
"authorization_url": "https://example.com/auth20250101.pdf 注意:请不要使用示例链接,示例链接仅作为参考格式。必须为实际的被查询人授权具有法律效益的授权书文件链接,如访问不到或为不实授权书将追究责任。协议必须为http https",
|
||||
"user_type": "1",
|
||||
"vehicle_type": "0",
|
||||
"page_num": "1",
|
||||
"page_size": "10",
|
||||
"use_scenario": "1",
|
||||
"auth_authorize_file_code": "AUTH123456",
|
||||
"plate_no": "京A12345",
|
||||
"plate_type": "01",
|
||||
"vin_code": "LSGBF53M8DS123456",
|
||||
"return_type": "1",
|
||||
"photo_data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
||||
"ownerType": "1",
|
||||
"type": "per",
|
||||
"query_reason_id": "1",
|
||||
"flag": "4",
|
||||
"dir": "down",
|
||||
"min_percent": "0",
|
||||
"max_percent": "1",
|
||||
"engine_number": "1234567890",
|
||||
"notice_model": "1",
|
||||
"vlphoto_data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
||||
"carplate_type": "01",
|
||||
"image_url": "https://example.com/images/driving_license.jpg",
|
||||
"reg_url": "https://example.com/images/vehicle_registration.jpg",
|
||||
"token": "0fc79b80371f45e2ac1c693ef9136b24",
|
||||
"vehicle_name": "车型名称,示例:凌派 2020款 锐·混动 1.5L 锐·舒适版",
|
||||
"vehicle_location": "车辆所在地,示例:北京",
|
||||
"first_registrationdate": "初登日期,示例:2020-05",
|
||||
"color": "示例:白色",
|
||||
"plate_color": "车牌颜色(0:蓝色,1:黄色,2:黑色,3:白色,4:渐变绿色,5:黄绿双拼色,6:蓝白渐变色,7:临时牌照,11:绿色,12:红色)默认标准车牌查蓝色,新能源车牌查绿色)",
|
||||
"mobile_no": "13800138000",
|
||||
"id_card": "110101199001011234",
|
||||
"idCard": "110101199001011234",
|
||||
"name": "张三",
|
||||
"man_name": "张三",
|
||||
"woman_name": "李四",
|
||||
"ent_name": "示例企业有限公司",
|
||||
"legal_person": "王五",
|
||||
"ent_code": "91110000123456789X",
|
||||
"ent_reg_no": "110000000123456",
|
||||
"auth_date": "20240101-20241231",
|
||||
"date_range": "20240101-20241231",
|
||||
"time_range": "09:00-18:00",
|
||||
"authorized": "1",
|
||||
"years": "5",
|
||||
"bank_card": "6222021234567890123",
|
||||
"mobile_type": "移动",
|
||||
"start_date": "2024-01-01",
|
||||
"unique_id": "UNIQUE123456",
|
||||
"return_url": "https://example.com/return",
|
||||
"authorization_url": "https://example.com/auth20250101.pdf 注意:请不要使用示例链接,示例链接仅作为参考格式。必须为实际的被查询人授权具有法律效益的授权书文件链接,如访问不到或为不实授权书将追究责任。协议必须为http https",
|
||||
"user_type": "1",
|
||||
"vehicle_type": "0",
|
||||
"page_num": "1",
|
||||
"page_size": "10",
|
||||
"use_scenario": "1",
|
||||
"auth_authorize_file_code": "AUTH123456",
|
||||
"plate_no": "京A12345",
|
||||
"plate_type": "01",
|
||||
"vin_code": "LSGBF53M8DS123456",
|
||||
"return_type": "1",
|
||||
"photo_data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
||||
"ownerType": "1",
|
||||
"type": "per",
|
||||
"query_reason_id": "1",
|
||||
"flag": "4",
|
||||
"dir": "down",
|
||||
"min_percent": "0",
|
||||
"max_percent": "1",
|
||||
"engine_number": "1234567890",
|
||||
"notice_model": "1",
|
||||
"vlphoto_data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
||||
"carplate_type": "01",
|
||||
"image_url": "https://example.com/images/driving_license.jpg",
|
||||
"reg_url": "https://example.com/images/vehicle_registration.jpg",
|
||||
"token": "0fc79b80371f45e2ac1c693ef9136b24",
|
||||
"vehicle_name": "车型名称,示例:凌派 2020款 锐·混动 1.5L 锐·舒适版",
|
||||
"vehicle_location": "车辆所在地,示例:北京",
|
||||
"first_registrationdate": "初登日期,示例:2020-05",
|
||||
"color": "示例:白色",
|
||||
"plate_color": "0",
|
||||
"marital_type": "10",
|
||||
"auth_authorize_file_base64": "JVBERi0xLjQKJcTl8uXr...(示例PDF的Base64编码)",
|
||||
}
|
||||
|
||||
if example, exists := exampleMap[jsonTag]; exists {
|
||||
@@ -572,56 +596,59 @@ func (s *FormConfigServiceImpl) generateExampleValue(fieldType reflect.Type, jso
|
||||
// generatePlaceholder 生成占位符
|
||||
func (s *FormConfigServiceImpl) generatePlaceholder(jsonTag string, fieldType string) string {
|
||||
placeholderMap := map[string]string{
|
||||
"mobile_no": "请输入11位手机号码",
|
||||
"id_card": "请输入18位身份证号码",
|
||||
"name": "请输入真实姓名",
|
||||
"man_name": "请输入男方真实姓名",
|
||||
"woman_name": "请输入女方真实姓名",
|
||||
"ent_name": "请输入企业全称",
|
||||
"legal_person": "请输入法人真实姓名",
|
||||
"ent_code": "请输入统一社会信用代码",
|
||||
"ent_reg_no": "请输入企业注册号(统一社会信用代码)",
|
||||
"auth_date": "请输入授权日期范围(YYYYMMDD-YYYYMMDD)",
|
||||
"date_range": "请输入日期范围(YYYYMMDD-YYYYMMDD)",
|
||||
"time_range": "请输入时间范围(HH:MM-HH:MM)",
|
||||
"authorized": "请选择是否授权",
|
||||
"years": "请输入查询年数(0-100)",
|
||||
"bank_card": "请输入银行卡号",
|
||||
"mobile_type": "请选择手机类型",
|
||||
"start_date": "请选择开始日期",
|
||||
"unique_id": "请输入唯一标识",
|
||||
"return_url": "请输入返回链接",
|
||||
"authorization_url": "请输入授权链接",
|
||||
"user_type": "请选择关系类型",
|
||||
"vehicle_type": "请选择车辆类型",
|
||||
"page_num": "请输入页码",
|
||||
"page_size": "请输入每页数量(1-100)",
|
||||
"use_scenario": "请选择使用场景",
|
||||
"auth_authorize_file_code": "请输入授权文件编码",
|
||||
"plate_no": "请输入车牌号",
|
||||
"plate_type": "请选择号牌类型(01或02)",
|
||||
"vin_code": "请输入17位车辆识别代号VIN码",
|
||||
"return_type": "请选择返回类型",
|
||||
"photo_data": "请输入base64编码的入参图片(支持JPG、BMP、PNG格式)",
|
||||
"ownerType": "请选择企业主类型",
|
||||
"type": "请选择查询类型",
|
||||
"query_reason_id": "请选择查询原因ID",
|
||||
"flag": "请输入层次(最大4)",
|
||||
"dir": "请选择方向(up-向上,down-向下)",
|
||||
"min_percent": "请输入股权穿透比例下限(默认0)",
|
||||
"max_percent": "请输入股权穿透比例上限(默认1)",
|
||||
"engine_number": "请输入发动机号码",
|
||||
"notice_model": "请输入车辆型号",
|
||||
"vlphoto_data": "请输入行驶证图片",
|
||||
"carplate_type": "请选择车辆号牌类型(01-大型汽车 02-小型汽车 03-使馆汽车 04-领馆汽车 05-境外汽车 06-外籍汽车 07-普通摩托车 08-轻便摩托车 09-使馆摩托车 10-领馆摩托车 11-境外摩托车 12-外籍摩托车 13-低速车 14-拖拉机 15-挂车 16-教练汽车 17-教练摩托车 20-临时入境汽车 21-临时入境摩托车 22-临时行驶车 23-警用汽车 24-警用摩托 51-新能源大型车 52-新能源小型车)",
|
||||
"image_url": "请输入行驶证图片地址",
|
||||
"reg_url": "请输入车辆登记证图片地址",
|
||||
"token": "请输入token",
|
||||
"vehicle_name": "请输入车型名称",
|
||||
"vehicle_location": "请输入车辆所在地",
|
||||
"first_registrationdate": "请输入首次登记日期,格式:YYYY-MM",
|
||||
"color": "请输入颜色",
|
||||
"plate_color": "请输入车牌颜色",
|
||||
"mobile_no": "请输入11位手机号码",
|
||||
"id_card": "请输入18位身份证号码",
|
||||
"idCard": "请输入18位身份证号码",
|
||||
"name": "请输入真实姓名",
|
||||
"man_name": "请输入男方真实姓名",
|
||||
"woman_name": "请输入女方真实姓名",
|
||||
"ent_name": "请输入企业全称",
|
||||
"legal_person": "请输入法人真实姓名",
|
||||
"ent_code": "请输入统一社会信用代码",
|
||||
"ent_reg_no": "请输入企业注册号(统一社会信用代码)",
|
||||
"auth_date": "请输入授权日期范围(YYYYMMDD-YYYYMMDD)",
|
||||
"date_range": "请输入日期范围(YYYYMMDD-YYYYMMDD)",
|
||||
"time_range": "请输入时间范围(HH:MM-HH:MM)",
|
||||
"authorized": "请选择是否授权",
|
||||
"years": "请输入查询年数(0-100)",
|
||||
"bank_card": "请输入银行卡号",
|
||||
"mobile_type": "请选择手机类型",
|
||||
"start_date": "请选择开始日期",
|
||||
"unique_id": "请输入唯一标识",
|
||||
"return_url": "请输入返回链接",
|
||||
"authorization_url": "请输入授权链接",
|
||||
"user_type": "请选择关系类型",
|
||||
"vehicle_type": "请选择车辆类型",
|
||||
"page_num": "请输入页码",
|
||||
"page_size": "请输入每页数量(1-100)",
|
||||
"use_scenario": "请选择使用场景",
|
||||
"auth_authorize_file_code": "请输入授权文件编码",
|
||||
"plate_no": "请输入车牌号",
|
||||
"plate_type": "请选择号牌类型(01或02)",
|
||||
"vin_code": "请输入17位车辆识别代号VIN码",
|
||||
"return_type": "请选择返回类型",
|
||||
"photo_data": "请输入base64编码的入参图片(支持JPG、BMP、PNG格式)",
|
||||
"ownerType": "请选择企业主类型",
|
||||
"type": "请选择查询类型",
|
||||
"query_reason_id": "请选择查询原因ID",
|
||||
"flag": "请输入层次(最大4)",
|
||||
"dir": "请选择方向(up-向上,down-向下)",
|
||||
"min_percent": "请输入股权穿透比例下限(默认0)",
|
||||
"max_percent": "请输入股权穿透比例上限(默认1)",
|
||||
"engine_number": "请输入发动机号码",
|
||||
"notice_model": "请输入车辆型号",
|
||||
"vlphoto_data": "请输入行驶证图片",
|
||||
"carplate_type": "请选择车辆号牌类型(01-大型汽车 02-小型汽车 03-使馆汽车 04-领馆汽车 05-境外汽车 06-外籍汽车 07-普通摩托车 08-轻便摩托车 09-使馆摩托车 10-领馆摩托车 11-境外摩托车 12-外籍摩托车 13-低速车 14-拖拉机 15-挂车 16-教练汽车 17-教练摩托车 20-临时入境汽车 21-临时入境摩托车 22-临时行驶车 23-警用汽车 24-警用摩托 51-新能源大型车 52-新能源小型车)",
|
||||
"image_url": "请输入入参图片地址",
|
||||
"reg_url": "请输入车辆登记证图片地址",
|
||||
"token": "请输入token",
|
||||
"vehicle_name": "请输入车型名称",
|
||||
"vehicle_location": "请输入车辆所在地",
|
||||
"first_registrationdate": "请输入首次登记日期,格式:YYYY-MM",
|
||||
"color": "请输入颜色",
|
||||
"plate_color": "请输入车牌颜色",
|
||||
"marital_type": "请选择婚姻状况类型",
|
||||
"auth_authorize_file_base64": "请输入PDF文件的Base64编码字符串",
|
||||
}
|
||||
|
||||
if placeholder, exists := placeholderMap[jsonTag]; exists {
|
||||
@@ -646,56 +673,59 @@ func (s *FormConfigServiceImpl) generatePlaceholder(jsonTag string, fieldType st
|
||||
// generateDescription 生成字段描述
|
||||
func (s *FormConfigServiceImpl) generateDescription(jsonTag string, validation string) string {
|
||||
descMap := map[string]string{
|
||||
"mobile_no": "请输入11位手机号码",
|
||||
"id_card": "请输入18位身份证号码最后一位如是字母请大写",
|
||||
"name": "请输入真实姓名",
|
||||
"man_name": "请输入男方真实姓名",
|
||||
"woman_name": "请输入女方真实姓名",
|
||||
"ent_name": "请输入企业全称",
|
||||
"legal_person": "请输入法人真实姓名",
|
||||
"ent_code": "请输入统一社会信用代码",
|
||||
"ent_reg_no": "请输入企业注册号(统一社会信用代码)",
|
||||
"auth_date": "请输入授权日期范围,格式:YYYYMMDD-YYYYMMDD,且日期范围必须包括今天",
|
||||
"date_range": "请输入日期范围,格式:YYYYMMDD-YYYYMMDD",
|
||||
"time_range": "请输入时间范围,格式:HH:MM-HH:MM",
|
||||
"authorized": "请输入是否授权:0-未授权,1-已授权",
|
||||
"years": "请输入查询年数(0-100)",
|
||||
"bank_card": "请输入银行卡号",
|
||||
"mobile_type": "请选择手机类型",
|
||||
"start_date": "请选择开始日期",
|
||||
"unique_id": "请输入唯一标识",
|
||||
"return_url": "请输入返回链接",
|
||||
"authorization_url": "请输入授权链接",
|
||||
"user_type": "关系类型:1-ETC开户人;2-车辆所有人;3-ETC经办人(默认1-ETC开户人)",
|
||||
"vehicle_type": "车辆类型:0-客车;1-货车;2-全部(默认查全部)",
|
||||
"page_num": "请输入页码,从1开始",
|
||||
"page_size": "请输入每页数量,范围1-100",
|
||||
"use_scenario": "使用场景:1-信贷审核;2-保险评估;3-招聘背景调查;4-其他业务场景;99-其他",
|
||||
"auth_authorize_file_code": "请输入授权文件编码",
|
||||
"plate_no": "请输入车牌号",
|
||||
"plate_type": "号牌类型:01-小型汽车;02-大型汽车(可选)",
|
||||
"vin_code": "请输入17位车辆识别代号VIN码(Vehicle Identification Number)",
|
||||
"return_type": "返回类型:1-专业和学校名称数据返回编码形式(默认);2-专业和学校名称数据返回中文名称",
|
||||
"photo_data": "入参图片:base64编码的图片数据,仅支持JPG、BMP、PNG三种格式",
|
||||
"owner_type": "企业主类型编码:1-法定代表人;2-主要人员;3-自然人股东;4-法定代表人及自然人股东;5-其他",
|
||||
"type": "查询类型:per-人员,ent-企业 ",
|
||||
"query_reason_id": "查询原因ID:1-授信审批;2-贷中管理;3-贷后管理;4-异议处理;5-担保查询;6-租赁资质审查;7-融资租赁审批;8-借贷撮合查询;9-保险审批;10-资质审核;11-风控审核;12-企业背调",
|
||||
"flag": "层次,最大4",
|
||||
"dir": "方向:up-向上穿透,down-向下穿透",
|
||||
"min_percent": "股权穿透比例下限(大于等于),默认为0,支持小数点后两位(以小数指代百分比)",
|
||||
"max_percent": "股权穿透比例上限(小于等于),默认为1,支持小数点后两位(以小数指代百分比)",
|
||||
"engine_number": "发动机号码",
|
||||
"notice_model": "车辆型号",
|
||||
"vlphoto_data": "行驶证图片:base64编码的图片数据,仅支持JPG、BMP、PNG三种格式",
|
||||
"carplate_type": "车辆号牌类型:01-大型汽车;02-小型汽车;03-使馆汽车;04-领馆汽车;05-境外汽车;06-外籍汽车;07-普通摩托车;08-轻便摩托车;09-使馆摩托车;10-领馆摩托车;11-境外摩托车;12-外籍摩托车;13-低速车;14-拖拉机;15-挂车;16-教练汽车;17-教练摩托车;20-临时入境汽车;21-临时入境摩托车;22-临时行驶车;23-警用汽车;24-警用摩托;51-新能源大型车;52-新能源小型车",
|
||||
"image_url": "入参图片url地址",
|
||||
"reg_url": "车辆登记证图片地址(非必填):请提供车辆登记证的图片URL地址",
|
||||
"token": "token采集及获取结果时所使用的凭证,有效期2个小时,在此时效内,应用侧可以发起采集请求(重复的采集所触发的结果会被忽略)和结果查询",
|
||||
"vehicle_name": "车型名称,示例:凌派 2020款 锐·混动 1.5L 锐·舒适版",
|
||||
"vehicle_location": "车辆所在地",
|
||||
"first_registrationdate": "首次登记日期,格式:YYYY-MM",
|
||||
"color": "颜色",
|
||||
"plate_color": "车牌颜色",
|
||||
"mobile_no": "请输入11位手机号码",
|
||||
"id_card": "请输入18位身份证号码最后一位如是字母请大写",
|
||||
"idCard": "请输入18位身份证号码最后一位如是字母请大写",
|
||||
"name": "请输入真实姓名",
|
||||
"man_name": "请输入男方真实姓名",
|
||||
"woman_name": "请输入女方真实姓名",
|
||||
"ent_name": "请输入企业全称",
|
||||
"legal_person": "请输入法人真实姓名",
|
||||
"ent_code": "请输入统一社会信用代码",
|
||||
"ent_reg_no": "请输入企业注册号(统一社会信用代码)",
|
||||
"auth_date": "请输入授权日期范围,格式:YYYYMMDD-YYYYMMDD,且日期范围必须包括今天",
|
||||
"date_range": "请输入日期范围,格式:YYYYMMDD-YYYYMMDD",
|
||||
"time_range": "请输入时间范围,格式:HH:MM-HH:MM",
|
||||
"authorized": "请输入是否授权:0-未授权,1-已授权",
|
||||
"years": "请输入查询年数(0-100)",
|
||||
"bank_card": "请输入银行卡号",
|
||||
"mobile_type": "请选择手机类型",
|
||||
"start_date": "请选择开始日期",
|
||||
"unique_id": "请输入唯一标识",
|
||||
"return_url": "请输入返回链接",
|
||||
"authorization_url": "请输入授权链接",
|
||||
"user_type": "关系类型:1-ETC开户人;2-车辆所有人;3-ETC经办人(默认1-ETC开户人)",
|
||||
"vehicle_type": "车辆类型:0-客车;1-货车;2-全部(默认查全部)",
|
||||
"page_num": "请输入页码,从1开始",
|
||||
"page_size": "请输入每页数量,范围1-100",
|
||||
"use_scenario": "使用场景:1-信贷审核;2-保险评估;3-招聘背景调查;4-其他业务场景;99-其他",
|
||||
"auth_authorize_file_code": "请输入授权文件编码",
|
||||
"plate_no": "请输入车牌号",
|
||||
"plate_type": "号牌类型:01-小型汽车;02-大型汽车(可选)",
|
||||
"vin_code": "请输入17位车辆识别代号VIN码(Vehicle Identification Number)",
|
||||
"return_type": "返回类型:1-专业和学校名称数据返回编码形式(默认);2-专业和学校名称数据返回中文名称",
|
||||
"photo_data": "入参图片:base64编码的图片数据,仅支持JPG、BMP、PNG三种格式",
|
||||
"owner_type": "企业主类型编码:1-法定代表人;2-主要人员;3-自然人股东;4-法定代表人及自然人股东;5-其他",
|
||||
"type": "查询类型:per-人员,ent-企业 ",
|
||||
"query_reason_id": "查询原因ID:1-授信审批;2-贷中管理;3-贷后管理;4-异议处理;5-担保查询;6-租赁资质审查;7-融资租赁审批;8-借贷撮合查询;9-保险审批;10-资质审核;11-风控审核;12-企业背调",
|
||||
"flag": "层次,最大4",
|
||||
"dir": "方向:up-向上穿透,down-向下穿透",
|
||||
"min_percent": "股权穿透比例下限(大于等于),默认为0,支持小数点后两位(以小数指代百分比)",
|
||||
"max_percent": "股权穿透比例上限(小于等于),默认为1,支持小数点后两位(以小数指代百分比)",
|
||||
"engine_number": "发动机号码",
|
||||
"notice_model": "车辆型号",
|
||||
"vlphoto_data": "行驶证图片:base64编码的图片数据,仅支持JPG、BMP、PNG三种格式",
|
||||
"carplate_type": "车辆号牌类型:01-大型汽车;02-小型汽车;03-使馆汽车;04-领馆汽车;05-境外汽车;06-外籍汽车;07-普通摩托车;08-轻便摩托车;09-使馆摩托车;10-领馆摩托车;11-境外摩托车;12-外籍摩托车;13-低速车;14-拖拉机;15-挂车;16-教练汽车;17-教练摩托车;20-临时入境汽车;21-临时入境摩托车;22-临时行驶车;23-警用汽车;24-警用摩托;51-新能源大型车;52-新能源小型车",
|
||||
"image_url": "入参图片url地址",
|
||||
"reg_url": "车辆登记证图片地址(非必填):请提供车辆登记证的图片URL地址",
|
||||
"token": "token采集及获取结果时所使用的凭证,有效期2个小时,在此时效内,应用侧可以发起采集请求(重复的采集所触发的结果会被忽略)和结果查询",
|
||||
"vehicle_name": "车型名称,示例:凌派 2020款 锐·混动 1.5L 锐·舒适版",
|
||||
"vehicle_location": "车辆所在地",
|
||||
"first_registrationdate": "首次登记日期,格式:YYYY-MM",
|
||||
"color": "颜色",
|
||||
"plate_color": "车牌颜色",
|
||||
"marital_type": "婚姻状况类型:10-未登记(无登记记录),20-已婚,30-丧偶,40-离异",
|
||||
"auth_authorize_file_base64": "请输入PDF文件的Base64编码字符串",
|
||||
}
|
||||
|
||||
if desc, exists := descMap[jsonTag]; exists {
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
"tyapi-server/internal/infrastructure/external/alicloud"
|
||||
"tyapi-server/internal/infrastructure/external/jiguang"
|
||||
"tyapi-server/internal/infrastructure/external/muzi"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
"tyapi-server/internal/infrastructure/external/tianyancha"
|
||||
"tyapi-server/internal/infrastructure/external/westdex"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
"tyapi-server/internal/infrastructure/external/xingwei"
|
||||
"tyapi-server/internal/infrastructure/external/yushan"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
@@ -47,6 +47,12 @@ type ProcessorDependencies struct {
|
||||
|
||||
// 企业报告记录仓储,用于持久化 QYGLJ1U9 生成的企业报告
|
||||
ReportRepo repositories.ReportRepository
|
||||
|
||||
// 企业报告 PDF 异步预生成(可为 nil)
|
||||
ReportPDFScheduler QYGLReportPDFScheduler
|
||||
|
||||
// APIPublicBaseURL 对外 API 根地址(无尾斜杠),用于 QYGL reportUrl 等
|
||||
APIPublicBaseURL string
|
||||
}
|
||||
|
||||
// NewProcessorDependencies 创建处理器依赖容器
|
||||
@@ -64,23 +70,27 @@ func NewProcessorDependencies(
|
||||
validator interfaces.RequestValidator,
|
||||
combService CombServiceInterface, // Changed to interface
|
||||
reportRepo repositories.ReportRepository,
|
||||
reportPDFScheduler QYGLReportPDFScheduler,
|
||||
apiPublicBaseURL string,
|
||||
) *ProcessorDependencies {
|
||||
return &ProcessorDependencies{
|
||||
WestDexService: westDexService,
|
||||
ShujubaoService: shujubaoService,
|
||||
MuziService: muziService,
|
||||
YushanService: yushanService,
|
||||
TianYanChaService: tianYanChaService,
|
||||
AlicloudService: alicloudService,
|
||||
ZhichaService: zhichaService,
|
||||
XingweiService: xingweiService,
|
||||
JiguangService: jiguangService,
|
||||
ShumaiService: shumaiService,
|
||||
Validator: validator,
|
||||
CombService: combService,
|
||||
Options: nil, // 初始化为nil,在调用时设置
|
||||
CallContext: nil, // 初始化为nil,在调用时设置
|
||||
ReportRepo: reportRepo,
|
||||
WestDexService: westDexService,
|
||||
ShujubaoService: shujubaoService,
|
||||
MuziService: muziService,
|
||||
YushanService: yushanService,
|
||||
TianYanChaService: tianYanChaService,
|
||||
AlicloudService: alicloudService,
|
||||
ZhichaService: zhichaService,
|
||||
XingweiService: xingweiService,
|
||||
JiguangService: jiguangService,
|
||||
ShumaiService: shumaiService,
|
||||
Validator: validator,
|
||||
CombService: combService,
|
||||
Options: nil, // 初始化为nil,在调用时设置
|
||||
CallContext: nil, // 初始化为nil,在调用时设置
|
||||
ReportRepo: reportRepo,
|
||||
ReportPDFScheduler: reportPDFScheduler,
|
||||
APIPublicBaseURL: apiPublicBaseURL,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package dwbg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
)
|
||||
|
||||
// ProcessDWBG5SAMRequest DWBG5SAM 天远指迷报告
|
||||
func ProcessDWBG5SAMRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.DWBG5SAMReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
encryptedMobileNo, err := deps.ZhichaService.Encrypt(paramsDto.MobileNo)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"name": encryptedName,
|
||||
"idCard": encryptedIDCard,
|
||||
"phone": encryptedMobileNo,
|
||||
"accessoryUrl": paramsDto.AuthorizationURL,
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI112", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, zhicha.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤响应数据,删除指定字段
|
||||
if respMap, ok := respData.(map[string]interface{}); ok {
|
||||
delete(respMap, "reportUrl")
|
||||
}
|
||||
|
||||
// 将响应数据转换为JSON字节
|
||||
respBytes, err := json.Marshal(respData)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// ProcessFLXG0V4BRequest FLXG0V4B API处理方法
|
||||
// ProcessFLXG0V4BRequest FLXG0V4B API处理方法(身份证排空入口,身份证身份证身份证身份证身份证)
|
||||
func ProcessFLXG0V4BRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.FLXG0V4BReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
@@ -24,7 +24,8 @@ func ProcessFLXG0V4BRequest(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998"{
|
||||
// 去掉司法案件案件去掉身份证号码
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" {
|
||||
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
|
||||
}
|
||||
encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name)
|
||||
|
||||
@@ -20,7 +20,9 @@ func ProcessFLXG3A9BRequest(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
if paramsDto.IDCard == "410482198504029333" {
|
||||
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
|
||||
}
|
||||
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
|
||||
@@ -20,7 +20,7 @@ func ProcessFLXG5A3BRequest(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550"|| paramsDto.IDCard == "320682198910134998"{
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" {
|
||||
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
|
||||
}
|
||||
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/xingwei"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
)
|
||||
|
||||
// ProcessFLXG7E8FRequest FLXG7E8F API处理方法 - 个人司法数据查询
|
||||
@@ -20,30 +20,225 @@ func ProcessFLXG7E8FRequest(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550"|| paramsDto.IDCard == "320682198910134998" {
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" {
|
||||
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
|
||||
}
|
||||
// 构建请求数据,将项目规范的字段名转换为 XingweiService 需要的字段名
|
||||
reqData := map[string]interface{}{
|
||||
"name": paramsDto.Name,
|
||||
"idCardNum": paramsDto.IDCard,
|
||||
"phoneNumber": paramsDto.MobileNo,
|
||||
|
||||
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
// 调用行为数据API,使用指定的project_id
|
||||
projectID := "CDJ-1101695378264092672"
|
||||
respBytes, err := deps.XingweiService.CallAPI(ctx, projectID, reqData)
|
||||
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
|
||||
if err != nil {
|
||||
if errors.Is(err, xingwei.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
} else if errors.Is(err, xingwei.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"name": encryptedName,
|
||||
"idCard": encryptedIDCard,
|
||||
"authorized": "1",
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI006", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, zhicha.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else if errors.Is(err, xingwei.ErrSystem) {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
respMap, ok := respData.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, errors.Join(processors.ErrSystem, errors.New("响应格式错误"))
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"judicial_data": mapFLXG5A3BToJudicialData(respMap),
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
|
||||
func mapFLXG5A3BToJudicialData(resp map[string]interface{}) map[string]interface{} {
|
||||
judicialData := map[string]interface{}{
|
||||
"consumptionRestrictionList": mapXgbzxrToConsumptionRestriction(asSlice(resp["xgbzxr"])),
|
||||
"breachCaseList": mapSxbzxrToBreachCaseList(asSlice(resp["sxbzxr"])),
|
||||
"lawsuitStat": normalizeLawsuitStat(asMap(resp["entout"])),
|
||||
}
|
||||
|
||||
return judicialData
|
||||
}
|
||||
|
||||
func mapXgbzxrToConsumptionRestriction(items []interface{}) []map[string]interface{} {
|
||||
result := make([]map[string]interface{}, 0, len(items))
|
||||
for _, item := range items {
|
||||
m := asMap(item)
|
||||
if len(m) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, map[string]interface{}{
|
||||
"caseNumber": m["ah"],
|
||||
"id": m["id"],
|
||||
"issueDate": m["fbrq"],
|
||||
"executiveCourt": m["zxfy"],
|
||||
"fileDate": m["larq"],
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func mapSxbzxrToBreachCaseList(items []interface{}) []map[string]interface{} {
|
||||
result := make([]map[string]interface{}, 0, len(items))
|
||||
for _, item := range items {
|
||||
m := asMap(item)
|
||||
if len(m) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, map[string]interface{}{
|
||||
"caseNumber": m["ah"],
|
||||
"issueDate": m["fbrq"],
|
||||
"id": m["id"],
|
||||
"fileDate": m["larq"],
|
||||
"fulfillStatus": m["lxqk"],
|
||||
"estimatedJudgementAmount": m["pjje_gj"],
|
||||
"province": m["sf"],
|
||||
"sex": m["xb"],
|
||||
"concreteDetails": m["xwqx"],
|
||||
"obligation": m["yw"],
|
||||
"executiveCourt": m["zxfy"],
|
||||
"enforcementBasisOrganization": m["zxyjdw"],
|
||||
"enforcementBasisNumber": m["zxyjwh"],
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func normalizeLawsuitStat(entout map[string]interface{}) map[string]interface{} {
|
||||
lawsuitStat := defaultLawsuitStat()
|
||||
for k, v := range entout {
|
||||
switch k {
|
||||
case "cases_tree":
|
||||
lawsuitStat[k] = normalizeCasesTree(asMap(v))
|
||||
case "count":
|
||||
lawsuitStat[k] = normalizeCount(asMap(v))
|
||||
case "preservation", "administrative", "civil", "implement", "criminal", "bankrupt":
|
||||
lawsuitStat[k] = normalizeCaseSection(asMap(v))
|
||||
default:
|
||||
lawsuitStat[k] = v
|
||||
}
|
||||
}
|
||||
return lawsuitStat
|
||||
}
|
||||
|
||||
func defaultLawsuitStat() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"crc": 0,
|
||||
"cases_tree": normalizeCasesTree(map[string]interface{}{}),
|
||||
"count": normalizeCount(map[string]interface{}{}),
|
||||
"preservation": normalizeCaseSection(map[string]interface{}{}),
|
||||
"administrative": normalizeCaseSection(map[string]interface{}{}),
|
||||
"civil": normalizeCaseSection(map[string]interface{}{}),
|
||||
"implement": normalizeCaseSection(map[string]interface{}{}),
|
||||
"criminal": normalizeCaseSection(map[string]interface{}{}),
|
||||
"bankrupt": normalizeCaseSection(map[string]interface{}{}),
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeCasesTree(src map[string]interface{}) map[string]interface{} {
|
||||
dst := map[string]interface{}{
|
||||
"administrative": []interface{}{},
|
||||
"criminal": []interface{}{},
|
||||
"civil": []interface{}{},
|
||||
}
|
||||
for _, key := range []string{"administrative", "criminal", "civil"} {
|
||||
if v, ok := src[key]; ok {
|
||||
dst[key] = asSlice(v)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func normalizeCaseSection(src map[string]interface{}) map[string]interface{} {
|
||||
dst := map[string]interface{}{
|
||||
"cases": []interface{}{},
|
||||
"count": normalizeCount(map[string]interface{}{}),
|
||||
}
|
||||
if v, ok := src["cases"]; ok {
|
||||
dst["cases"] = asSlice(v)
|
||||
}
|
||||
if v, ok := src["count"]; ok {
|
||||
dst["count"] = normalizeCount(asMap(v))
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func normalizeCount(src map[string]interface{}) map[string]interface{} {
|
||||
dst := map[string]interface{}{
|
||||
"money_yuangao": 0,
|
||||
"area_stat": "",
|
||||
"count_jie_beigao": 0,
|
||||
"count_total": 0,
|
||||
"money_wei_yuangao": 0,
|
||||
"count_wei_total": 0,
|
||||
"money_wei_beigao": 0,
|
||||
"count_other": 0,
|
||||
"money_beigao": 0,
|
||||
"count_yuangao": 0,
|
||||
"money_jie_other": 0,
|
||||
"money_total": 0,
|
||||
"money_wei_total": 0,
|
||||
"count_wei_yuangao": 0,
|
||||
"ay_stat": "",
|
||||
"count_beigao": 0,
|
||||
"money_jie_yuangao": 0,
|
||||
"jafs_stat": "",
|
||||
"money_jie_beigao": 0,
|
||||
"count_wei_beigao": 0,
|
||||
"count_jie_other": 0,
|
||||
"count_jie_total": 0,
|
||||
"count_wei_other": 0,
|
||||
"money_other": 0,
|
||||
"count_jie_yuangao": 0,
|
||||
"money_jie_total": 0,
|
||||
"money_wei_other": 0,
|
||||
"money_wei_percent": 0,
|
||||
"larq_stat": "",
|
||||
}
|
||||
for k, v := range src {
|
||||
dst[k] = v
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func asMap(v interface{}) map[string]interface{} {
|
||||
if v == nil {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
|
||||
if m, ok := v.(map[string]interface{}); ok {
|
||||
return m
|
||||
}
|
||||
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
|
||||
func asSlice(v interface{}) []interface{} {
|
||||
if v == nil {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
if s, ok := v.([]interface{}); ok {
|
||||
return s
|
||||
}
|
||||
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@ func ProcessFLXG9C1DRequest(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
if paramsDto.IDCard == "410482198504029333" {
|
||||
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
|
||||
}
|
||||
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
|
||||
@@ -20,7 +20,7 @@ func ProcessFLXGCA3DRequest(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550"|| paramsDto.IDCard == "320682198910134998" {
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" {
|
||||
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
|
||||
}
|
||||
encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name)
|
||||
|
||||
@@ -25,7 +25,7 @@ func ProcessFLXGDEA9Request(ctx context.Context, params []byte, deps *processors
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550"|| paramsDto.IDCard == "320682198910134998"{
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" {
|
||||
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
|
||||
}
|
||||
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package flxg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessFLXGDJG3Request FLXGDJG3 董监高司法综合信息核验 API 处理方法(使用数据宝服务示例)
|
||||
func ProcessFLXGDJG3Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.FLXGDJG3Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建数据宝入参(sign 外的业务参数可按需 AES 加密后作为 bodyData)
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "1cce582f0a6f3ca40de80f1bea9b9698",
|
||||
"idcard": paramsDto.IDCard,
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/communication/personal/10166"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
// 解析响应中的 JSON 字符串(使用 qyglb4c0 中的 RecursiveParse)
|
||||
parsedResp, err := RecursiveParse(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(parsedResp)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -20,7 +20,9 @@ func ProcessFLXGK5D2Request(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
if paramsDto.IDCard == "410482198504029333" {
|
||||
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
|
||||
}
|
||||
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package ivyz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessIVYZ18HYRequest IVYZ18HY 婚姻状况核验V2(单人) API 处理方法(使用数据宝服务示例)
|
||||
func ProcessIVYZ18HYRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.IVYZ18HYReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
fixedData := map[string]interface{}{"msg": "请联系商务咨询"}
|
||||
fixedRespBytes, err := json.Marshal(fixedData)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return fixedRespBytes, nil
|
||||
|
||||
authDate := ""
|
||||
if len(paramsDto.AuthDate) >= 8 {
|
||||
authDate = paramsDto.AuthDate[len(paramsDto.AuthDate)-8:]
|
||||
}
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "",
|
||||
"idcard": paramsDto.IDCard,
|
||||
"name": paramsDto.Name,
|
||||
"maritalType": paramsDto.MaritalType,
|
||||
"authcode": paramsDto.AuthAuthorizeFileBase64,
|
||||
"authAuthorizeFileCode": paramsDto.AuthAuthorizeFileCode,
|
||||
"authDate": authDate,
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/communication/personal/10333"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package ivyz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessIVYZ28HYRequest IVYZ28HY 婚姻状况核验单人) API 处理方法(使用数据宝服务示例)
|
||||
func ProcessIVYZ28HYRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.IVYZ28HYReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
fixedData := map[string]interface{}{"msg": "请联系商务咨询"}
|
||||
fixedRespBytes, err := json.Marshal(fixedData)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return fixedRespBytes, nil
|
||||
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "",
|
||||
"idcard": paramsDto.IDCard,
|
||||
"name": paramsDto.Name,
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/communication/personal/10149"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -40,11 +40,11 @@ func ProcessIVYZ2A8BRequest(ctx context.Context, params []byte, deps *processors
|
||||
|
||||
// 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded
|
||||
apiPath := "/v4/id_card/check" // 接口路径,根据数脉文档填写(如 v4/xxx)
|
||||
|
||||
|
||||
// 先尝试使用政务接口(app_id2 和 app_secret2)
|
||||
respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData, true)
|
||||
if err != nil {
|
||||
// 使用实时接口(app_id 和 app_secret)重试
|
||||
// 使用实时接口(app_id 和 app_secret)重试
|
||||
respBytes, err = deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData, false)
|
||||
// 如果重试后仍然失败,或者原本就是查无记录错误,返回错误
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package ivyz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
)
|
||||
|
||||
// ProcessIVYZ2MN6Request IVYZ2MN6 API处理方法
|
||||
func ProcessIVYZ2MN6Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.IVYZ2MN6Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"name": encryptedName,
|
||||
"idCard": encryptedIDCard,
|
||||
"authorized": paramsDto.Authorized,
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI1004", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, zhicha.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(respData)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package ivyz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessIVYZ38SRRequest IVYZ38SR 婚姻状态核验(双人) API 处理方法(使用数据宝服务示例)
|
||||
func ProcessIVYZ38SRRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.IVYZ38SRReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
fixedData := map[string]interface{}{"msg": "请联系商务咨询"}
|
||||
fixedRespBytes, err := json.Marshal(fixedData)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return fixedRespBytes, nil
|
||||
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "",
|
||||
"name": paramsDto.ManName,
|
||||
"idcard": paramsDto.ManIDCard,
|
||||
"woman_name": paramsDto.WomanName,
|
||||
"woman_idcard": paramsDto.WomanIDCard,
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/communication/personal/10148"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package ivyz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/muzi"
|
||||
)
|
||||
|
||||
// ProcessIVYZ3P9MRequest IVYZ3P9M API处理方法 - 学历查询实时版
|
||||
func ProcessIVYZ3P9MRequest_2(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.IVYZ3P9MReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
encryptedName, err := deps.MuziService.Encrypt(paramsDto.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
encryptedCertCode, err := deps.MuziService.Encrypt(paramsDto.IDCard)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
// 处理 returnType 参数,默认为 "1"
|
||||
returnType := paramsDto.ReturnType
|
||||
if returnType == "" {
|
||||
returnType = "1"
|
||||
}
|
||||
paramSign := map[string]interface{}{
|
||||
"returnType": returnType,
|
||||
"realName": encryptedName,
|
||||
"certCode": encryptedCertCode,
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"realName": encryptedName,
|
||||
"certCode": encryptedCertCode,
|
||||
"returnType": returnType,
|
||||
}
|
||||
|
||||
respData, err := deps.MuziService.CallAPI(ctx, "PC0041", "/academic", reqData, paramSign)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, muzi.ErrDatasource):
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
case errors.Is(err, muzi.ErrSystem):
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
default:
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
return respData, nil
|
||||
}
|
||||
@@ -4,10 +4,11 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/muzi"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
)
|
||||
|
||||
// ProcessIVYZ3P9MRequest IVYZ3P9M API处理方法 - 学历查询实时版
|
||||
@@ -21,45 +22,147 @@ func ProcessIVYZ3P9MRequest(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
encryptedName, err := deps.MuziService.Encrypt(paramsDto.Name)
|
||||
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
encryptedCertCode, err := deps.MuziService.Encrypt(paramsDto.IDCard)
|
||||
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
// 处理 returnType 参数,默认为 "1"
|
||||
returnType := paramsDto.ReturnType
|
||||
if returnType == "" {
|
||||
returnType = "1"
|
||||
}
|
||||
paramSign := map[string]interface{}{
|
||||
"returnType": returnType,
|
||||
"realName": encryptedName,
|
||||
"certCode": encryptedCertCode,
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"realName": encryptedName,
|
||||
"certCode": encryptedCertCode,
|
||||
"returnType": returnType,
|
||||
"name": encryptedName,
|
||||
"idCard": encryptedIDCard,
|
||||
"authorized": "1",
|
||||
}
|
||||
|
||||
|
||||
respData, err := deps.MuziService.CallAPI(ctx, "PC0041", "/academic",reqData,paramSign)
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI1004", reqData)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, muzi.ErrDatasource):
|
||||
if errors.Is(err, zhicha.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
case errors.Is(err, muzi.ErrSystem):
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
default:
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
out, err := mapZCI1004ToIVYZ3P9M(respData, paramsDto.Name, paramsDto.IDCard)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
type zci1004Item struct {
|
||||
EndDate string `json:"endDate"`
|
||||
EducationLevel string `json:"educationLevel"`
|
||||
LearningForm string `json:"learningForm"`
|
||||
}
|
||||
|
||||
type ivyz3p9mItem struct {
|
||||
GraduationDate string `json:"graduationDate"`
|
||||
StudentName string `json:"studentName"`
|
||||
EducationLevel string `json:"educationLevel"`
|
||||
LearningForm string `json:"learningForm"`
|
||||
IDNumber string `json:"idNumber"`
|
||||
}
|
||||
|
||||
func mapZCI1004ToIVYZ3P9M(respData interface{}, name, idCard string) ([]ivyz3p9mItem, error) {
|
||||
respBytes, err := json.Marshal(respData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var source []zci1004Item
|
||||
if err := json.Unmarshal(respBytes, &source); err != nil {
|
||||
var wrapped struct {
|
||||
Data []zci1004Item `json:"data"`
|
||||
}
|
||||
if err2 := json.Unmarshal(respBytes, &wrapped); err2 != nil {
|
||||
return nil, err
|
||||
}
|
||||
source = wrapped.Data
|
||||
}
|
||||
|
||||
out := make([]ivyz3p9mItem, 0, len(source))
|
||||
for _, it := range source {
|
||||
out = append(out, ivyz3p9mItem{
|
||||
GraduationDate: normalizeDateDigits(it.EndDate),
|
||||
StudentName: name,
|
||||
EducationLevel: mapEducationLevelToCode(it.EducationLevel),
|
||||
LearningForm: mapLearningFormToCode(it.LearningForm),
|
||||
IDNumber: idCard,
|
||||
})
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func mapEducationLevelToCode(level string) string {
|
||||
v := normalizeText(level)
|
||||
switch {
|
||||
case strings.Contains(v, "第二学士"):
|
||||
return "5"
|
||||
case strings.Contains(v, "博士"):
|
||||
return "4"
|
||||
case strings.Contains(v, "硕士"):
|
||||
return "3"
|
||||
case strings.Contains(v, "本科"):
|
||||
return "2"
|
||||
case strings.Contains(v, "专科"), strings.Contains(v, "大专"):
|
||||
return "1"
|
||||
default:
|
||||
return "99"
|
||||
}
|
||||
}
|
||||
|
||||
func mapLearningFormToCode(form string) string {
|
||||
v := normalizeText(form)
|
||||
switch {
|
||||
case strings.Contains(v, "脱产"):
|
||||
return "1"
|
||||
case strings.Contains(v, "普通全日制"):
|
||||
return "2"
|
||||
case strings.Contains(v, "全日制"):
|
||||
return "3"
|
||||
case strings.Contains(v, "开放教育"), strings.Contains(v, "开放大学"):
|
||||
return "4"
|
||||
case strings.Contains(v, "夜大学"), strings.Contains(v, "夜大"):
|
||||
return "5"
|
||||
case strings.Contains(v, "函授"):
|
||||
return "6"
|
||||
case strings.Contains(v, "网络教育"), strings.Contains(v, "网教"), strings.Contains(v, "远程教育"):
|
||||
return "7"
|
||||
case strings.Contains(v, "非全日制"):
|
||||
return "8"
|
||||
case strings.Contains(v, "业余"):
|
||||
return "9"
|
||||
case strings.Contains(v, "自学考试"), strings.Contains(v, "自考"):
|
||||
// 自考在既有枚举中无直对应,兼容并入“业余”
|
||||
return "9"
|
||||
default:
|
||||
return "99"
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeDateDigits(s string) string {
|
||||
trimmed := strings.TrimSpace(s)
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
for _, ch := range trimmed {
|
||||
if ch >= '0' && ch <= '9' {
|
||||
b.WriteRune(ch)
|
||||
}
|
||||
}
|
||||
|
||||
return respData, nil
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func normalizeText(s string) string {
|
||||
v := strings.TrimSpace(strings.ToLower(s))
|
||||
v = strings.ReplaceAll(v, " ", "")
|
||||
v = strings.ReplaceAll(v, "-", "")
|
||||
v = strings.ReplaceAll(v, "_", "")
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package ivyz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessIVYZ48SRRequest IVYZ48SR 婚姻状态核验V2(双人) API 处理方法(使用数据宝服务示例)
|
||||
func ProcessIVYZ48SRRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.IVYZ48SRReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
fixedData := map[string]interface{}{"msg": "请联系商务咨询"}
|
||||
fixedRespBytes, err := json.Marshal(fixedData)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return fixedRespBytes, nil
|
||||
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "",
|
||||
"name": paramsDto.ManName,
|
||||
"idcard": paramsDto.ManIDCard,
|
||||
"woman_name": paramsDto.WomanName,
|
||||
"woman_idcard": paramsDto.WomanIDCard,
|
||||
"marital_type": paramsDto.MaritalType,
|
||||
"auth_authorize_file_code": paramsDto.AuthAuthorizeFileCode,
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/communication/personal/10332"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package ivyz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
)
|
||||
|
||||
// ProcessIVYZ5E22Request API处理方法 - 双人婚姻评估查询
|
||||
func ProcessIVYZ5E22Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.IVYZ5E22Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
encryptedManName, err := deps.ZhichaService.Encrypt(paramsDto.ManName)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
encryptedManIDCard, err := deps.ZhichaService.Encrypt(paramsDto.ManIDCard)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
encryptedWomanName, err := deps.ZhichaService.Encrypt(paramsDto.WomanName)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
encryptedWomanIDCard, err := deps.ZhichaService.Encrypt(paramsDto.WomanIDCard)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"nameMan": encryptedManName,
|
||||
"idCardMan": encryptedManIDCard,
|
||||
"nameWoman": encryptedWomanName,
|
||||
"idCardWoman": encryptedWomanIDCard,
|
||||
"authorized": paramsDto.Authorized,
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI042", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, zhicha.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 将响应数据转换为JSON字节
|
||||
respBytes, err := json.Marshal(respData)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -49,6 +49,7 @@ func ProcessIVYZ81NCRequest(ctx context.Context, params []byte, deps *processors
|
||||
// 解析响应数据,期望格式为 {"state": "1"}
|
||||
var stateResp struct {
|
||||
State string `json:"state"`
|
||||
RegTime string `json:"regTime"`
|
||||
}
|
||||
|
||||
// 将 respData 转换为 JSON 字节再解析
|
||||
@@ -82,7 +83,7 @@ func ProcessIVYZ81NCRequest(ctx context.Context, params []byte, deps *processors
|
||||
result := map[string]interface{}{
|
||||
"code": "0",
|
||||
"data": map[string]interface{}{
|
||||
"op_date": "",
|
||||
"op_date": stateResp.RegTime,
|
||||
"op_type": opType,
|
||||
"op_type_desc": opTypeDesc,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package ivyz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
)
|
||||
|
||||
// ProcessIVYZFIC1Request IVYZFIC1 人脸身份证比对 API 处理方法(数脉)
|
||||
func ProcessIVYZFIC1Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.IVYZFIC1Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
if strings.TrimSpace(paramsDto.PhotoData) == "" && strings.TrimSpace(paramsDto.ImageUrl) == "" {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, errors.New("image和url至少传一个"))
|
||||
}
|
||||
|
||||
reqFormData := map[string]interface{}{
|
||||
"idcard": paramsDto.IDCard,
|
||||
"name": paramsDto.Name,
|
||||
"image": paramsDto.PhotoData,
|
||||
"url": paramsDto.ImageUrl,
|
||||
}
|
||||
|
||||
apiPath := "/v4/face_id_card/compare"
|
||||
|
||||
// 先尝试政务接口,再回退实时接口
|
||||
respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData, true)
|
||||
if err != nil {
|
||||
respBytes, err = deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData, false)
|
||||
if err != nil {
|
||||
if errors.Is(err, shumai.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
} else if errors.Is(err, shumai.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else if errors.Is(err, shumai.ErrSystem) {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -20,36 +20,102 @@ func ProcessIVYZN2P8Request(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
reqFormData := map[string]interface{}{
|
||||
"idcard": paramsDto.IDCard,
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"name": paramsDto.Name,
|
||||
"idcard": paramsDto.IDCard,
|
||||
}
|
||||
|
||||
// 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded
|
||||
apiPath := "/v4/id_card/check" // 接口路径,根据数脉文档填写(如 v4/xxx)
|
||||
|
||||
apiPath := "/v4/id_card/check"
|
||||
|
||||
// 先尝试使用政务接口(app_id2 和 app_secret2)
|
||||
respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData, true)
|
||||
respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqData, true)
|
||||
if err != nil {
|
||||
// 使用实时接口(app_id 和 app_secret)重试
|
||||
respBytes, err = deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData, false)
|
||||
// 如果重试后仍然失败,返回错误
|
||||
if err != nil {
|
||||
if errors.Is(err, shumai.ErrNotFound) {
|
||||
// 查无记录情况
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
} else if errors.Is(err, shumai.ErrDatasource) {
|
||||
// 数据源错误
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else if errors.Is(err, shumai.ErrSystem) {
|
||||
// 系统错误
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
} else {
|
||||
// 其他未知错误
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
if errors.Is(err, shumai.ErrNotFound) {
|
||||
// 查无记录情况
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
} else if errors.Is(err, shumai.ErrDatasource) {
|
||||
// 数据源错误
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else if errors.Is(err, shumai.ErrSystem) {
|
||||
// 系统错误
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
|
||||
// respBytes, err := deps.AlicloudService.CallAPI("api-mall/api/id_card/check", reqData)
|
||||
// if err != nil {
|
||||
// if errors.Is(err, alicloud.ErrDatasource) {
|
||||
// return nil, errors.Join(processors.ErrDatasource, err)
|
||||
// }
|
||||
// return nil, errors.Join(processors.ErrSystem, err)
|
||||
// }
|
||||
|
||||
// return respBytes, nil
|
||||
// // 对齐 yysybe08test 的原始响应结构,取 data 字段映射为 ivyzn2p8 返回
|
||||
// var aliyunData struct {
|
||||
// Code int `json:"code"`
|
||||
// Data struct {
|
||||
// Birthday string `json:"birthday"`
|
||||
// Result interface{} `json:"result"`
|
||||
// Address string `json:"address"`
|
||||
// OrderNo string `json:"orderNo"`
|
||||
// Sex string `json:"sex"`
|
||||
// Desc string `json:"desc"`
|
||||
// } `json:"data"`
|
||||
// Result interface{} `json:"result"`
|
||||
// Desc string `json:"desc"`
|
||||
// }
|
||||
// if err := json.Unmarshal(respBytes, &aliyunData); err != nil {
|
||||
// return nil, errors.Join(processors.ErrSystem, err)
|
||||
// }
|
||||
|
||||
// rawResult := aliyunData.Result
|
||||
// rawDesc := aliyunData.Desc
|
||||
// if aliyunData.Code == 200 {
|
||||
// rawResult = aliyunData.Data.Result
|
||||
// rawDesc = aliyunData.Data.Desc
|
||||
// }
|
||||
|
||||
// response := map[string]interface{}{
|
||||
// "result": normalizeResult(rawResult),
|
||||
// "order_no": aliyunData.Data.OrderNo,
|
||||
// "desc": rawDesc,
|
||||
// "sex": aliyunData.Data.Sex,
|
||||
// "birthday": aliyunData.Data.Birthday,
|
||||
// "address": aliyunData.Data.Address,
|
||||
// }
|
||||
// return json.Marshal(response)
|
||||
// }
|
||||
|
||||
// func normalizeResult(v interface{}) int {
|
||||
// switch r := v.(type) {
|
||||
// case float64:
|
||||
// return int(r)
|
||||
// case int:
|
||||
// return r
|
||||
// case int32:
|
||||
// return int(r)
|
||||
// case int64:
|
||||
// return int(r)
|
||||
// case json.Number:
|
||||
// n, err := r.Int64()
|
||||
// if err == nil {
|
||||
// return int(n)
|
||||
// }
|
||||
// case string:
|
||||
// s := strings.TrimSpace(r)
|
||||
// if s == "" {
|
||||
// return 1
|
||||
// }
|
||||
// n, err := strconv.Atoi(s)
|
||||
// if err == nil {
|
||||
// return n
|
||||
// }
|
||||
// }
|
||||
// // 默认按不一致处理
|
||||
// return 1
|
||||
// }
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package ivyz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
)
|
||||
|
||||
// ProcessIVYZRAX1Request IVYZRAX1 API处理方法 - 融安信用分
|
||||
func ProcessIVYZRAX1Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.IVYZRAX1Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
|
||||
// if err != nil {
|
||||
// return nil, errors.Join(processors.ErrSystem, err)
|
||||
// }
|
||||
|
||||
// encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
|
||||
// if err != nil {
|
||||
// return nil, errors.Join(processors.ErrSystem, err)
|
||||
// }
|
||||
// encryptedMoblie, err := deps.ZhichaService.Encrypt(paramsDto.MobileNo)
|
||||
// if err != nil {
|
||||
// return nil, errors.Join(processors.ErrSystem, err)
|
||||
// }
|
||||
|
||||
md5Name := deps.ZhichaService.MD5(paramsDto.Name)
|
||||
md5IDCard := deps.ZhichaService.MD5(paramsDto.IDCard)
|
||||
md5Mobile := deps.ZhichaService.MD5(paramsDto.MobileNo)
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
// "name": encryptedName,
|
||||
// "idCard": encryptedIDCard,
|
||||
// "phone": encryptedMoblie,
|
||||
"authorized": paramsDto.Authorized,
|
||||
"name": md5Name,
|
||||
"idCard": md5IDCard,
|
||||
"phone": md5Mobile,
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI084", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, zhicha.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 将响应数据转换为JSON字节
|
||||
respBytes, err := json.Marshal(respData)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package ivyz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
)
|
||||
|
||||
// ProcessIVYZRAX2Request IVYZRAX2 API处理方法 - 融御反欺诈分
|
||||
func ProcessIVYZRAX2Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.IVYZRAX1Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
|
||||
// if err != nil {
|
||||
// return nil, errors.Join(processors.ErrSystem, err)
|
||||
// }
|
||||
|
||||
// encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
|
||||
// if err != nil {
|
||||
// return nil, errors.Join(processors.ErrSystem, err)
|
||||
// }
|
||||
// encryptedMoblie, err := deps.ZhichaService.Encrypt(paramsDto.MobileNo)
|
||||
// if err != nil {
|
||||
// return nil, errors.Join(processors.ErrSystem, err)
|
||||
// }
|
||||
|
||||
md5Name := deps.ZhichaService.MD5(paramsDto.Name)
|
||||
md5IDCard := deps.ZhichaService.MD5(paramsDto.IDCard)
|
||||
md5Mobile := deps.ZhichaService.MD5(paramsDto.MobileNo)
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
// "name": encryptedName,
|
||||
// "idCard": encryptedIDCard,
|
||||
// "phone": encryptedMoblie,
|
||||
"authorized": paramsDto.Authorized,
|
||||
"name": md5Name,
|
||||
"idCard": md5IDCard,
|
||||
"phone": md5Mobile,
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI083", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, zhicha.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 将响应数据转换为JSON字节
|
||||
respBytes, err := json.Marshal(respData)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -4,10 +4,15 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/xingwei"
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
)
|
||||
|
||||
// ProcessIVYZZQT3Request IVYZZQT3 人脸比对V3API处理方法
|
||||
@@ -21,31 +26,187 @@ func ProcessIVYZZQT3Request(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建请求数据,使用xingwei服务的正确字段名
|
||||
reqData := map[string]interface{}{
|
||||
"name": paramsDto.Name,
|
||||
"idCardNum": paramsDto.IDCard,
|
||||
"image": paramsDto.PhotoData,
|
||||
// 使用数脉接口进行人脸身份证比对
|
||||
reqFormData := map[string]interface{}{
|
||||
"idcard": paramsDto.IDCard,
|
||||
"name": paramsDto.Name,
|
||||
"image": paramsDto.PhotoData,
|
||||
}
|
||||
|
||||
// 调用行为数据API,使用指定的project_id
|
||||
projectID := "CDJ-1104321430396268544"
|
||||
respBytes, err := deps.XingweiService.CallAPI(ctx, projectID, reqData)
|
||||
apiPath := "/v4/face_id_card/compare"
|
||||
|
||||
// 先尝试政务接口,再回退实时接口
|
||||
respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData, true)
|
||||
if err != nil {
|
||||
if errors.Is(err, xingwei.ErrNotFound) {
|
||||
// 查空情况,返回特定的查空错误
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
} else if errors.Is(err, xingwei.ErrDatasource) {
|
||||
// 数据源错误
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else if errors.Is(err, xingwei.ErrSystem) {
|
||||
// 系统错误
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
} else {
|
||||
// 其他未知错误
|
||||
respBytes, err = deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData, false)
|
||||
if err != nil {
|
||||
if errors.Is(err, shumai.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
} else if errors.Is(err, shumai.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else if errors.Is(err, shumai.ErrSystem) {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
outBytes, err := mapShumaiFaceCompareToIVYZZQT3(respBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return outBytes, nil
|
||||
}
|
||||
|
||||
type shumaiFaceCompareResp struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
Score interface{} `json:"score"`
|
||||
Msg string `json:"msg"`
|
||||
Incorrect interface{} `json:"incorrect"`
|
||||
}
|
||||
|
||||
type ivyzzqt3Out struct {
|
||||
HandleTime string `json:"handleTime"`
|
||||
ResultData ivyzzqt3OutResultData `json:"resultData"`
|
||||
OrderNo string `json:"orderNo"`
|
||||
}
|
||||
|
||||
type ivyzzqt3OutResultData struct {
|
||||
VerificationCode string `json:"verification_code"`
|
||||
VerificationResult string `json:"verification_result"`
|
||||
VerificationMessage string `json:"verification_message"`
|
||||
Similarity string `json:"similarity"`
|
||||
}
|
||||
|
||||
func mapShumaiFaceCompareToIVYZZQT3(respBytes []byte) ([]byte, error) {
|
||||
var r shumaiFaceCompareResp
|
||||
if err := json.Unmarshal(respBytes, &r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
score := parseScoreToFloat64(r.Score)
|
||||
similarity := strconv.Itoa(int(math.Round(mapScoreToSimilarity(score))))
|
||||
verificationResult := mapScoreToVerificationResult(score)
|
||||
verificationMessage := strings.TrimSpace(r.Msg)
|
||||
if verificationMessage == "" {
|
||||
verificationMessage = mapScoreToVerificationMessage(score)
|
||||
}
|
||||
|
||||
out := ivyzzqt3Out{
|
||||
HandleTime: time.Now().Format("2006-01-02 15:04:05"),
|
||||
OrderNo: strings.TrimSpace(r.OrderNo),
|
||||
ResultData: ivyzzqt3OutResultData{
|
||||
VerificationCode: mapVerificationCode(verificationResult, r.Incorrect),
|
||||
VerificationResult: verificationResult,
|
||||
VerificationMessage: verificationMessage,
|
||||
Similarity: similarity,
|
||||
},
|
||||
}
|
||||
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
func mapScoreToVerificationResult(score float64) string {
|
||||
if score >= 0.45 {
|
||||
return "valid"
|
||||
}
|
||||
// 旧结构仅支持 valid/invalid,不能确定场景按 invalid 返回
|
||||
return "invalid"
|
||||
}
|
||||
|
||||
func mapScoreToVerificationMessage(score float64) string {
|
||||
if score < 0.40 {
|
||||
return "系统判断为不同人"
|
||||
}
|
||||
if score < 0.45 {
|
||||
return "不能确定是否为同一人"
|
||||
}
|
||||
return "系统判断为同一人"
|
||||
}
|
||||
|
||||
func mapScoreToSimilarity(score float64) float64 {
|
||||
// 将 score(0~1) 分段映射到 similarity(0~1000),并对齐业务阈值:
|
||||
// 0.40 -> 600,0.45 -> 700
|
||||
if score <= 0 {
|
||||
return 0
|
||||
}
|
||||
if score >= 1 {
|
||||
return 1000
|
||||
}
|
||||
if score < 0.40 {
|
||||
// [0, 0.40) -> [0, 600)
|
||||
return (score / 0.40) * 600
|
||||
}
|
||||
if score < 0.45 {
|
||||
// [0.40, 0.45) -> [600, 700)
|
||||
return 600 + ((score-0.40)/0.05)*100
|
||||
}
|
||||
// [0.45, 1] -> [700, 1000]
|
||||
return 700 + ((score-0.45)/0.55)*300
|
||||
}
|
||||
|
||||
func parseScoreToFloat64(v interface{}) float64 {
|
||||
switch t := v.(type) {
|
||||
case float64:
|
||||
return t
|
||||
case float32:
|
||||
return float64(t)
|
||||
case int:
|
||||
return float64(t)
|
||||
case int32:
|
||||
return float64(t)
|
||||
case int64:
|
||||
return float64(t)
|
||||
case json.Number:
|
||||
if f, err := t.Float64(); err == nil {
|
||||
return f
|
||||
}
|
||||
case string:
|
||||
s := strings.TrimSpace(t)
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func valueToString(v interface{}) string {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(t)
|
||||
case json.Number:
|
||||
return t.String()
|
||||
case float64:
|
||||
return strconv.FormatFloat(t, 'f', -1, 64)
|
||||
case float32:
|
||||
return strconv.FormatFloat(float64(t), 'f', -1, 64)
|
||||
case int:
|
||||
return strconv.Itoa(t)
|
||||
case int32:
|
||||
return strconv.FormatInt(int64(t), 10)
|
||||
case int64:
|
||||
return strconv.FormatInt(t, 10)
|
||||
default:
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(fmt.Sprint(v))
|
||||
}
|
||||
}
|
||||
|
||||
func mapVerificationCode(verificationResult string, upstreamIncorrect interface{}) string {
|
||||
if verificationResult == "valid" {
|
||||
return "1000"
|
||||
}
|
||||
if verificationResult == "invalid" {
|
||||
return "2006"
|
||||
}
|
||||
// 兜底:若后续扩展出其它结果,保持可追溯
|
||||
if s := valueToString(upstreamIncorrect); s != "" {
|
||||
return s
|
||||
}
|
||||
return "2006"
|
||||
}
|
||||
|
||||
@@ -4,13 +4,15 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/xingwei"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
)
|
||||
|
||||
// ProcessJRZQ0L85Request JRZQ0L85 API处理方法 - xingwei service
|
||||
// ProcessJRZQ0L85Request JRZQ0L85 API处理方法 - 个人信用分
|
||||
func ProcessJRZQ0L85Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.JRZQ0L85Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
@@ -21,27 +23,100 @@ func ProcessJRZQ0L85Request(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建请求数据,将项目规范的字段名转换为 XingweiService 需要的字段名
|
||||
reqData := map[string]interface{}{
|
||||
"name": paramsDto.Name,
|
||||
"idCardNum": paramsDto.IDCard,
|
||||
"phoneNumber": paramsDto.MobileNo,
|
||||
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
// 调用行为数据API,使用指定的project_id
|
||||
projectID := "CDJ-1101695364016041984"
|
||||
respBytes, err := deps.XingweiService.CallAPI(ctx, projectID, reqData)
|
||||
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
|
||||
if err != nil {
|
||||
if errors.Is(err, xingwei.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
} else if errors.Is(err, xingwei.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
encryptedMobileNo, err := deps.ZhichaService.Encrypt(paramsDto.MobileNo)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"name": encryptedName,
|
||||
"idCard": encryptedIDCard,
|
||||
"phone": encryptedMobileNo,
|
||||
"authorized": "1",
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI021", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, zhicha.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else if errors.Is(err, xingwei.ErrSystem) {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
score := "-1"
|
||||
if m, ok := respData.(map[string]interface{}); ok {
|
||||
if raw, exists := m["xyp_cpl0081"]; exists {
|
||||
if v, ok := parseToFloat64(raw); ok {
|
||||
score = mapXypToGeneralScore(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"score_120_General": score,
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
|
||||
func parseToFloat64(v interface{}) (float64, bool) {
|
||||
switch value := v.(type) {
|
||||
case float64:
|
||||
return value, true
|
||||
case string:
|
||||
if value == "" {
|
||||
return 0, false
|
||||
}
|
||||
f, err := strconv.ParseFloat(value, 64)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return f, true
|
||||
case json.Number:
|
||||
f, err := value.Float64()
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return f, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func mapXypToGeneralScore(xyp float64) string {
|
||||
// xyp_cpl0081: 0~1,值越大风险越高;
|
||||
// score_120_General: 300~900,值越大信用越好。
|
||||
if xyp < 0 {
|
||||
xyp = 0
|
||||
}
|
||||
if xyp > 1 {
|
||||
xyp = 1
|
||||
}
|
||||
|
||||
score := 900 - xyp*600
|
||||
scoreInt := int(math.Round(score))
|
||||
if scoreInt < 300 {
|
||||
scoreInt = 300
|
||||
}
|
||||
if scoreInt > 900 {
|
||||
scoreInt = 900
|
||||
}
|
||||
|
||||
return strconv.Itoa(scoreInt)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,833 @@
|
||||
package jrzq
|
||||
|
||||
var jrzq6F2AVariableKeys = []string{
|
||||
"flag_applyloanstr",
|
||||
"als_d7_id_pdl_allnum",
|
||||
"als_d7_id_pdl_orgnum",
|
||||
"als_d7_id_caon_allnum",
|
||||
"als_d7_id_caon_orgnum",
|
||||
"als_d7_id_rel_allnum",
|
||||
"als_d7_id_rel_orgnum",
|
||||
"als_d7_id_caoff_allnum",
|
||||
"als_d7_id_caoff_orgnum",
|
||||
"als_d7_id_cooff_allnum",
|
||||
"als_d7_id_cooff_orgnum",
|
||||
"als_d7_id_af_allnum",
|
||||
"als_d7_id_af_orgnum",
|
||||
"als_d7_id_coon_allnum",
|
||||
"als_d7_id_coon_orgnum",
|
||||
"als_d7_id_oth_allnum",
|
||||
"als_d7_id_oth_orgnum",
|
||||
"als_d7_id_bank_selfnum",
|
||||
"als_d7_id_bank_allnum",
|
||||
"als_d7_id_bank_tra_allnum",
|
||||
"als_d7_id_bank_ret_allnum",
|
||||
"als_d7_id_bank_orgnum",
|
||||
"als_d7_id_bank_tra_orgnum",
|
||||
"als_d7_id_bank_ret_orgnum",
|
||||
"als_d7_id_bank_week_allnum",
|
||||
"als_d7_id_bank_week_orgnum",
|
||||
"als_d7_id_bank_night_allnum",
|
||||
"als_d7_id_bank_night_orgnum",
|
||||
"als_d7_id_nbank_selfnum",
|
||||
"als_d7_id_nbank_allnum",
|
||||
"als_d7_id_nbank_p2p_allnum",
|
||||
"als_d7_id_nbank_mc_allnum",
|
||||
"als_d7_id_nbank_ca_allnum",
|
||||
"als_d7_id_nbank_cf_allnum",
|
||||
"als_d7_id_nbank_com_allnum",
|
||||
"als_d7_id_nbank_oth_allnum",
|
||||
"als_d7_id_nbank_nsloan_allnum",
|
||||
"als_d7_id_nbank_autofin_allnum",
|
||||
"als_d7_id_nbank_sloan_allnum",
|
||||
"als_d7_id_nbank_cons_allnum",
|
||||
"als_d7_id_nbank_finlea_allnum",
|
||||
"als_d7_id_nbank_else_allnum",
|
||||
"als_d7_id_nbank_orgnum",
|
||||
"als_d7_id_nbank_p2p_orgnum",
|
||||
"als_d7_id_nbank_mc_orgnum",
|
||||
"als_d7_id_nbank_ca_orgnum",
|
||||
"als_d7_id_nbank_cf_orgnum",
|
||||
"als_d7_id_nbank_com_orgnum",
|
||||
"als_d7_id_nbank_oth_orgnum",
|
||||
"als_d7_id_nbank_nsloan_orgnum",
|
||||
"als_d7_id_nbank_autofin_orgnum",
|
||||
"als_d7_id_nbank_sloan_orgnum",
|
||||
"als_d7_id_nbank_cons_orgnum",
|
||||
"als_d7_id_nbank_finlea_orgnum",
|
||||
"als_d7_id_nbank_else_orgnum",
|
||||
"als_d7_id_nbank_week_allnum",
|
||||
"als_d7_id_nbank_week_orgnum",
|
||||
"als_d7_id_nbank_night_allnum",
|
||||
"als_d7_id_nbank_night_orgnum",
|
||||
"als_d7_cell_pdl_allnum",
|
||||
"als_d7_cell_pdl_orgnum",
|
||||
"als_d7_cell_caon_allnum",
|
||||
"als_d7_cell_caon_orgnum",
|
||||
"als_d7_cell_rel_allnum",
|
||||
"als_d7_cell_rel_orgnum",
|
||||
"als_d7_cell_caoff_allnum",
|
||||
"als_d7_cell_caoff_orgnum",
|
||||
"als_d7_cell_cooff_allnum",
|
||||
"als_d7_cell_cooff_orgnum",
|
||||
"als_d7_cell_af_allnum",
|
||||
"als_d7_cell_af_orgnum",
|
||||
"als_d7_cell_coon_allnum",
|
||||
"als_d7_cell_coon_orgnum",
|
||||
"als_d7_cell_oth_allnum",
|
||||
"als_d7_cell_oth_orgnum",
|
||||
"als_d7_cell_bank_selfnum",
|
||||
"als_d7_cell_bank_allnum",
|
||||
"als_d7_cell_bank_tra_allnum",
|
||||
"als_d7_cell_bank_ret_allnum",
|
||||
"als_d7_cell_bank_orgnum",
|
||||
"als_d7_cell_bank_tra_orgnum",
|
||||
"als_d7_cell_bank_ret_orgnum",
|
||||
"als_d7_cell_bank_week_allnum",
|
||||
"als_d7_cell_bank_week_orgnum",
|
||||
"als_d7_cell_bank_night_allnum",
|
||||
"als_d7_cell_bank_night_orgnum",
|
||||
"als_d7_cell_nbank_selfnum",
|
||||
"als_d7_cell_nbank_allnum",
|
||||
"als_d7_cell_nbank_p2p_allnum",
|
||||
"als_d7_cell_nbank_mc_allnum",
|
||||
"als_d7_cell_nbank_ca_allnum",
|
||||
"als_d7_cell_nbank_cf_allnum",
|
||||
"als_d7_cell_nbank_com_allnum",
|
||||
"als_d7_cell_nbank_oth_allnum",
|
||||
"als_d7_cell_nbank_nsloan_allnum",
|
||||
"als_d7_cell_nbank_autofin_allnum",
|
||||
"als_d7_cell_nbank_sloan_allnum",
|
||||
"als_d7_cell_nbank_cons_allnum",
|
||||
"als_d7_cell_nbank_finlea_allnum",
|
||||
"als_d7_cell_nbank_else_allnum",
|
||||
"als_d7_cell_nbank_orgnum",
|
||||
"als_d7_cell_nbank_p2p_orgnum",
|
||||
"als_d7_cell_nbank_mc_orgnum",
|
||||
"als_d7_cell_nbank_ca_orgnum",
|
||||
"als_d7_cell_nbank_cf_orgnum",
|
||||
"als_d7_cell_nbank_com_orgnum",
|
||||
"als_d7_cell_nbank_oth_orgnum",
|
||||
"als_d7_cell_nbank_nsloan_orgnum",
|
||||
"als_d7_cell_nbank_autofin_orgnum",
|
||||
"als_d7_cell_nbank_sloan_orgnum",
|
||||
"als_d7_cell_nbank_cons_orgnum",
|
||||
"als_d7_cell_nbank_finlea_orgnum",
|
||||
"als_d7_cell_nbank_else_orgnum",
|
||||
"als_d7_cell_nbank_week_allnum",
|
||||
"als_d7_cell_nbank_week_orgnum",
|
||||
"als_d7_cell_nbank_night_allnum",
|
||||
"als_d7_cell_nbank_night_orgnum",
|
||||
"als_d15_id_pdl_allnum",
|
||||
"als_d15_id_pdl_orgnum",
|
||||
"als_d15_id_caon_allnum",
|
||||
"als_d15_id_caon_orgnum",
|
||||
"als_d15_id_rel_allnum",
|
||||
"als_d15_id_rel_orgnum",
|
||||
"als_d15_id_caoff_allnum",
|
||||
"als_d15_id_caoff_orgnum",
|
||||
"als_d15_id_cooff_allnum",
|
||||
"als_d15_id_cooff_orgnum",
|
||||
"als_d15_id_af_allnum",
|
||||
"als_d15_id_af_orgnum",
|
||||
"als_d15_id_coon_allnum",
|
||||
"als_d15_id_coon_orgnum",
|
||||
"als_d15_id_oth_allnum",
|
||||
"als_d15_id_oth_orgnum",
|
||||
"als_d15_id_bank_selfnum",
|
||||
"als_d15_id_bank_allnum",
|
||||
"als_d15_id_bank_tra_allnum",
|
||||
"als_d15_id_bank_ret_allnum",
|
||||
"als_d15_id_bank_orgnum",
|
||||
"als_d15_id_bank_tra_orgnum",
|
||||
"als_d15_id_bank_ret_orgnum",
|
||||
"als_d15_id_bank_week_allnum",
|
||||
"als_d15_id_bank_week_orgnum",
|
||||
"als_d15_id_bank_night_allnum",
|
||||
"als_d15_id_bank_night_orgnum",
|
||||
"als_d15_id_nbank_selfnum",
|
||||
"als_d15_id_nbank_allnum",
|
||||
"als_d15_id_nbank_p2p_allnum",
|
||||
"als_d15_id_nbank_mc_allnum",
|
||||
"als_d15_id_nbank_ca_allnum",
|
||||
"als_d15_id_nbank_cf_allnum",
|
||||
"als_d15_id_nbank_com_allnum",
|
||||
"als_d15_id_nbank_oth_allnum",
|
||||
"als_d15_id_nbank_nsloan_allnum",
|
||||
"als_d15_id_nbank_autofin_allnum",
|
||||
"als_d15_id_nbank_sloan_allnum",
|
||||
"als_d15_id_nbank_cons_allnum",
|
||||
"als_d15_id_nbank_finlea_allnum",
|
||||
"als_d15_id_nbank_else_allnum",
|
||||
"als_d15_id_nbank_orgnum",
|
||||
"als_d15_id_nbank_p2p_orgnum",
|
||||
"als_d15_id_nbank_mc_orgnum",
|
||||
"als_d15_id_nbank_ca_orgnum",
|
||||
"als_d15_id_nbank_cf_orgnum",
|
||||
"als_d15_id_nbank_com_orgnum",
|
||||
"als_d15_id_nbank_oth_orgnum",
|
||||
"als_d15_id_nbank_nsloan_orgnum",
|
||||
"als_d15_id_nbank_autofin_orgnum",
|
||||
"als_d15_id_nbank_sloan_orgnum",
|
||||
"als_d15_id_nbank_cons_orgnum",
|
||||
"als_d15_id_nbank_finlea_orgnum",
|
||||
"als_d15_id_nbank_else_orgnum",
|
||||
"als_d15_id_nbank_week_allnum",
|
||||
"als_d15_id_nbank_week_orgnum",
|
||||
"als_d15_id_nbank_night_allnum",
|
||||
"als_d15_id_nbank_night_orgnum",
|
||||
"als_d15_cell_pdl_allnum",
|
||||
"als_d15_cell_pdl_orgnum",
|
||||
"als_d15_cell_caon_allnum",
|
||||
"als_d15_cell_caon_orgnum",
|
||||
"als_d15_cell_rel_allnum",
|
||||
"als_d15_cell_rel_orgnum",
|
||||
"als_d15_cell_caoff_allnum",
|
||||
"als_d15_cell_caoff_orgnum",
|
||||
"als_d15_cell_cooff_allnum",
|
||||
"als_d15_cell_cooff_orgnum",
|
||||
"als_d15_cell_af_allnum",
|
||||
"als_d15_cell_af_orgnum",
|
||||
"als_d15_cell_coon_allnum",
|
||||
"als_d15_cell_coon_orgnum",
|
||||
"als_d15_cell_oth_allnum",
|
||||
"als_d15_cell_oth_orgnum",
|
||||
"als_d15_cell_bank_selfnum",
|
||||
"als_d15_cell_bank_allnum",
|
||||
"als_d15_cell_bank_tra_allnum",
|
||||
"als_d15_cell_bank_ret_allnum",
|
||||
"als_d15_cell_bank_orgnum",
|
||||
"als_d15_cell_bank_tra_orgnum",
|
||||
"als_d15_cell_bank_ret_orgnum",
|
||||
"als_d15_cell_bank_week_allnum",
|
||||
"als_d15_cell_bank_week_orgnum",
|
||||
"als_d15_cell_bank_night_allnum",
|
||||
"als_d15_cell_bank_night_orgnum",
|
||||
"als_d15_cell_nbank_selfnum",
|
||||
"als_d15_cell_nbank_allnum",
|
||||
"als_d15_cell_nbank_p2p_allnum",
|
||||
"als_d15_cell_nbank_mc_allnum",
|
||||
"als_d15_cell_nbank_ca_allnum",
|
||||
"als_d15_cell_nbank_cf_allnum",
|
||||
"als_d15_cell_nbank_com_allnum",
|
||||
"als_d15_cell_nbank_oth_allnum",
|
||||
"als_d15_cell_nbank_nsloan_allnum",
|
||||
"als_d15_cell_nbank_autofin_allnum",
|
||||
"als_d15_cell_nbank_sloan_allnum",
|
||||
"als_d15_cell_nbank_cons_allnum",
|
||||
"als_d15_cell_nbank_finlea_allnum",
|
||||
"als_d15_cell_nbank_else_allnum",
|
||||
"als_d15_cell_nbank_orgnum",
|
||||
"als_d15_cell_nbank_p2p_orgnum",
|
||||
"als_d15_cell_nbank_mc_orgnum",
|
||||
"als_d15_cell_nbank_ca_orgnum",
|
||||
"als_d15_cell_nbank_cf_orgnum",
|
||||
"als_d15_cell_nbank_com_orgnum",
|
||||
"als_d15_cell_nbank_oth_orgnum",
|
||||
"als_d15_cell_nbank_nsloan_orgnum",
|
||||
"als_d15_cell_nbank_autofin_orgnum",
|
||||
"als_d15_cell_nbank_sloan_orgnum",
|
||||
"als_d15_cell_nbank_cons_orgnum",
|
||||
"als_d15_cell_nbank_finlea_orgnum",
|
||||
"als_d15_cell_nbank_else_orgnum",
|
||||
"als_d15_cell_nbank_week_allnum",
|
||||
"als_d15_cell_nbank_week_orgnum",
|
||||
"als_d15_cell_nbank_night_allnum",
|
||||
"als_d15_cell_nbank_night_orgnum",
|
||||
"als_m1_id_pdl_allnum",
|
||||
"als_m1_id_pdl_orgnum",
|
||||
"als_m1_id_caon_allnum",
|
||||
"als_m1_id_caon_orgnum",
|
||||
"als_m1_id_rel_allnum",
|
||||
"als_m1_id_rel_orgnum",
|
||||
"als_m1_id_caoff_allnum",
|
||||
"als_m1_id_caoff_orgnum",
|
||||
"als_m1_id_cooff_allnum",
|
||||
"als_m1_id_cooff_orgnum",
|
||||
"als_m1_id_af_allnum",
|
||||
"als_m1_id_af_orgnum",
|
||||
"als_m1_id_coon_allnum",
|
||||
"als_m1_id_coon_orgnum",
|
||||
"als_m1_id_oth_allnum",
|
||||
"als_m1_id_oth_orgnum",
|
||||
"als_m1_id_bank_selfnum",
|
||||
"als_m1_id_bank_allnum",
|
||||
"als_m1_id_bank_tra_allnum",
|
||||
"als_m1_id_bank_ret_allnum",
|
||||
"als_m1_id_bank_orgnum",
|
||||
"als_m1_id_bank_tra_orgnum",
|
||||
"als_m1_id_bank_ret_orgnum",
|
||||
"als_m1_id_bank_week_allnum",
|
||||
"als_m1_id_bank_week_orgnum",
|
||||
"als_m1_id_bank_night_allnum",
|
||||
"als_m1_id_bank_night_orgnum",
|
||||
"als_m1_id_nbank_selfnum",
|
||||
"als_m1_id_nbank_allnum",
|
||||
"als_m1_id_nbank_p2p_allnum",
|
||||
"als_m1_id_nbank_mc_allnum",
|
||||
"als_m1_id_nbank_ca_allnum",
|
||||
"als_m1_id_nbank_cf_allnum",
|
||||
"als_m1_id_nbank_com_allnum",
|
||||
"als_m1_id_nbank_oth_allnum",
|
||||
"als_m1_id_nbank_nsloan_allnum",
|
||||
"als_m1_id_nbank_autofin_allnum",
|
||||
"als_m1_id_nbank_sloan_allnum",
|
||||
"als_m1_id_nbank_cons_allnum",
|
||||
"als_m1_id_nbank_finlea_allnum",
|
||||
"als_m1_id_nbank_else_allnum",
|
||||
"als_m1_id_nbank_orgnum",
|
||||
"als_m1_id_nbank_p2p_orgnum",
|
||||
"als_m1_id_nbank_mc_orgnum",
|
||||
"als_m1_id_nbank_ca_orgnum",
|
||||
"als_m1_id_nbank_cf_orgnum",
|
||||
"als_m1_id_nbank_com_orgnum",
|
||||
"als_m1_id_nbank_oth_orgnum",
|
||||
"als_m1_id_nbank_nsloan_orgnum",
|
||||
"als_m1_id_nbank_autofin_orgnum",
|
||||
"als_m1_id_nbank_sloan_orgnum",
|
||||
"als_m1_id_nbank_cons_orgnum",
|
||||
"als_m1_id_nbank_finlea_orgnum",
|
||||
"als_m1_id_nbank_else_orgnum",
|
||||
"als_m1_id_nbank_week_allnum",
|
||||
"als_m1_id_nbank_week_orgnum",
|
||||
"als_m1_id_nbank_night_allnum",
|
||||
"als_m1_id_nbank_night_orgnum",
|
||||
"als_m1_cell_pdl_allnum",
|
||||
"als_m1_cell_pdl_orgnum",
|
||||
"als_m1_cell_caon_allnum",
|
||||
"als_m1_cell_caon_orgnum",
|
||||
"als_m1_cell_rel_allnum",
|
||||
"als_m1_cell_rel_orgnum",
|
||||
"als_m1_cell_caoff_allnum",
|
||||
"als_m1_cell_caoff_orgnum",
|
||||
"als_m1_cell_cooff_allnum",
|
||||
"als_m1_cell_cooff_orgnum",
|
||||
"als_m1_cell_af_allnum",
|
||||
"als_m1_cell_af_orgnum",
|
||||
"als_m1_cell_coon_allnum",
|
||||
"als_m1_cell_coon_orgnum",
|
||||
"als_m1_cell_oth_allnum",
|
||||
"als_m1_cell_oth_orgnum",
|
||||
"als_m1_cell_bank_selfnum",
|
||||
"als_m1_cell_bank_allnum",
|
||||
"als_m1_cell_bank_tra_allnum",
|
||||
"als_m1_cell_bank_ret_allnum",
|
||||
"als_m1_cell_bank_orgnum",
|
||||
"als_m1_cell_bank_tra_orgnum",
|
||||
"als_m1_cell_bank_ret_orgnum",
|
||||
"als_m1_cell_bank_week_allnum",
|
||||
"als_m1_cell_bank_week_orgnum",
|
||||
"als_m1_cell_bank_night_allnum",
|
||||
"als_m1_cell_bank_night_orgnum",
|
||||
"als_m1_cell_nbank_selfnum",
|
||||
"als_m1_cell_nbank_allnum",
|
||||
"als_m1_cell_nbank_p2p_allnum",
|
||||
"als_m1_cell_nbank_mc_allnum",
|
||||
"als_m1_cell_nbank_ca_allnum",
|
||||
"als_m1_cell_nbank_cf_allnum",
|
||||
"als_m1_cell_nbank_com_allnum",
|
||||
"als_m1_cell_nbank_oth_allnum",
|
||||
"als_m1_cell_nbank_nsloan_allnum",
|
||||
"als_m1_cell_nbank_autofin_allnum",
|
||||
"als_m1_cell_nbank_sloan_allnum",
|
||||
"als_m1_cell_nbank_cons_allnum",
|
||||
"als_m1_cell_nbank_finlea_allnum",
|
||||
"als_m1_cell_nbank_else_allnum",
|
||||
"als_m1_cell_nbank_orgnum",
|
||||
"als_m1_cell_nbank_p2p_orgnum",
|
||||
"als_m1_cell_nbank_mc_orgnum",
|
||||
"als_m1_cell_nbank_ca_orgnum",
|
||||
"als_m1_cell_nbank_cf_orgnum",
|
||||
"als_m1_cell_nbank_com_orgnum",
|
||||
"als_m1_cell_nbank_oth_orgnum",
|
||||
"als_m1_cell_nbank_nsloan_orgnum",
|
||||
"als_m1_cell_nbank_autofin_orgnum",
|
||||
"als_m1_cell_nbank_sloan_orgnum",
|
||||
"als_m1_cell_nbank_cons_orgnum",
|
||||
"als_m1_cell_nbank_finlea_orgnum",
|
||||
"als_m1_cell_nbank_else_orgnum",
|
||||
"als_m1_cell_nbank_week_allnum",
|
||||
"als_m1_cell_nbank_week_orgnum",
|
||||
"als_m1_cell_nbank_night_allnum",
|
||||
"als_m1_cell_nbank_night_orgnum",
|
||||
"als_m3_id_max_inteday",
|
||||
"als_m3_id_min_inteday",
|
||||
"als_m3_id_tot_mons",
|
||||
"als_m3_id_avg_monnum",
|
||||
"als_m3_id_max_monnum",
|
||||
"als_m3_id_min_monnum",
|
||||
"als_m3_id_pdl_allnum",
|
||||
"als_m3_id_pdl_orgnum",
|
||||
"als_m3_id_caon_allnum",
|
||||
"als_m3_id_caon_orgnum",
|
||||
"als_m3_id_rel_allnum",
|
||||
"als_m3_id_rel_orgnum",
|
||||
"als_m3_id_caoff_allnum",
|
||||
"als_m3_id_caoff_orgnum",
|
||||
"als_m3_id_cooff_allnum",
|
||||
"als_m3_id_cooff_orgnum",
|
||||
"als_m3_id_af_allnum",
|
||||
"als_m3_id_af_orgnum",
|
||||
"als_m3_id_coon_allnum",
|
||||
"als_m3_id_coon_orgnum",
|
||||
"als_m3_id_oth_allnum",
|
||||
"als_m3_id_oth_orgnum",
|
||||
"als_m3_id_bank_selfnum",
|
||||
"als_m3_id_bank_allnum",
|
||||
"als_m3_id_bank_tra_allnum",
|
||||
"als_m3_id_bank_ret_allnum",
|
||||
"als_m3_id_bank_orgnum",
|
||||
"als_m3_id_bank_tra_orgnum",
|
||||
"als_m3_id_bank_ret_orgnum",
|
||||
"als_m3_id_bank_tot_mons",
|
||||
"als_m3_id_bank_avg_monnum",
|
||||
"als_m3_id_bank_max_monnum",
|
||||
"als_m3_id_bank_min_monnum",
|
||||
"als_m3_id_bank_max_inteday",
|
||||
"als_m3_id_bank_min_inteday",
|
||||
"als_m3_id_bank_week_allnum",
|
||||
"als_m3_id_bank_week_orgnum",
|
||||
"als_m3_id_bank_night_allnum",
|
||||
"als_m3_id_bank_night_orgnum",
|
||||
"als_m3_id_nbank_selfnum",
|
||||
"als_m3_id_nbank_allnum",
|
||||
"als_m3_id_nbank_p2p_allnum",
|
||||
"als_m3_id_nbank_mc_allnum",
|
||||
"als_m3_id_nbank_ca_allnum",
|
||||
"als_m3_id_nbank_cf_allnum",
|
||||
"als_m3_id_nbank_com_allnum",
|
||||
"als_m3_id_nbank_oth_allnum",
|
||||
"als_m3_id_nbank_nsloan_allnum",
|
||||
"als_m3_id_nbank_autofin_allnum",
|
||||
"als_m3_id_nbank_sloan_allnum",
|
||||
"als_m3_id_nbank_cons_allnum",
|
||||
"als_m3_id_nbank_finlea_allnum",
|
||||
"als_m3_id_nbank_else_allnum",
|
||||
"als_m3_id_nbank_orgnum",
|
||||
"als_m3_id_nbank_p2p_orgnum",
|
||||
"als_m3_id_nbank_mc_orgnum",
|
||||
"als_m3_id_nbank_ca_orgnum",
|
||||
"als_m3_id_nbank_cf_orgnum",
|
||||
"als_m3_id_nbank_com_orgnum",
|
||||
"als_m3_id_nbank_oth_orgnum",
|
||||
"als_m3_id_nbank_nsloan_orgnum",
|
||||
"als_m3_id_nbank_autofin_orgnum",
|
||||
"als_m3_id_nbank_sloan_orgnum",
|
||||
"als_m3_id_nbank_cons_orgnum",
|
||||
"als_m3_id_nbank_finlea_orgnum",
|
||||
"als_m3_id_nbank_else_orgnum",
|
||||
"als_m3_id_nbank_tot_mons",
|
||||
"als_m3_id_nbank_avg_monnum",
|
||||
"als_m3_id_nbank_max_monnum",
|
||||
"als_m3_id_nbank_min_monnum",
|
||||
"als_m3_id_nbank_max_inteday",
|
||||
"als_m3_id_nbank_min_inteday",
|
||||
"als_m3_id_nbank_week_allnum",
|
||||
"als_m3_id_nbank_week_orgnum",
|
||||
"als_m3_id_nbank_night_allnum",
|
||||
"als_m3_id_nbank_night_orgnum",
|
||||
"als_m3_cell_max_inteday",
|
||||
"als_m3_cell_min_inteday",
|
||||
"als_m3_cell_tot_mons",
|
||||
"als_m3_cell_avg_monnum",
|
||||
"als_m3_cell_max_monnum",
|
||||
"als_m3_cell_min_monnum",
|
||||
"als_m3_cell_pdl_allnum",
|
||||
"als_m3_cell_pdl_orgnum",
|
||||
"als_m3_cell_caon_allnum",
|
||||
"als_m3_cell_caon_orgnum",
|
||||
"als_m3_cell_rel_allnum",
|
||||
"als_m3_cell_rel_orgnum",
|
||||
"als_m3_cell_caoff_allnum",
|
||||
"als_m3_cell_caoff_orgnum",
|
||||
"als_m3_cell_cooff_allnum",
|
||||
"als_m3_cell_cooff_orgnum",
|
||||
"als_m3_cell_af_allnum",
|
||||
"als_m3_cell_af_orgnum",
|
||||
"als_m3_cell_coon_allnum",
|
||||
"als_m3_cell_coon_orgnum",
|
||||
"als_m3_cell_oth_allnum",
|
||||
"als_m3_cell_oth_orgnum",
|
||||
"als_m3_cell_bank_selfnum",
|
||||
"als_m3_cell_bank_allnum",
|
||||
"als_m3_cell_bank_tra_allnum",
|
||||
"als_m3_cell_bank_ret_allnum",
|
||||
"als_m3_cell_bank_orgnum",
|
||||
"als_m3_cell_bank_tra_orgnum",
|
||||
"als_m3_cell_bank_ret_orgnum",
|
||||
"als_m3_cell_bank_tot_mons",
|
||||
"als_m3_cell_bank_avg_monnum",
|
||||
"als_m3_cell_bank_max_monnum",
|
||||
"als_m3_cell_bank_min_monnum",
|
||||
"als_m3_cell_bank_max_inteday",
|
||||
"als_m3_cell_bank_min_inteday",
|
||||
"als_m3_cell_bank_week_allnum",
|
||||
"als_m3_cell_bank_week_orgnum",
|
||||
"als_m3_cell_bank_night_allnum",
|
||||
"als_m3_cell_bank_night_orgnum",
|
||||
"als_m3_cell_nbank_selfnum",
|
||||
"als_m3_cell_nbank_allnum",
|
||||
"als_m3_cell_nbank_p2p_allnum",
|
||||
"als_m3_cell_nbank_mc_allnum",
|
||||
"als_m3_cell_nbank_ca_allnum",
|
||||
"als_m3_cell_nbank_cf_allnum",
|
||||
"als_m3_cell_nbank_com_allnum",
|
||||
"als_m3_cell_nbank_oth_allnum",
|
||||
"als_m3_cell_nbank_nsloan_allnum",
|
||||
"als_m3_cell_nbank_autofin_allnum",
|
||||
"als_m3_cell_nbank_sloan_allnum",
|
||||
"als_m3_cell_nbank_cons_allnum",
|
||||
"als_m3_cell_nbank_finlea_allnum",
|
||||
"als_m3_cell_nbank_else_allnum",
|
||||
"als_m3_cell_nbank_orgnum",
|
||||
"als_m3_cell_nbank_p2p_orgnum",
|
||||
"als_m3_cell_nbank_mc_orgnum",
|
||||
"als_m3_cell_nbank_ca_orgnum",
|
||||
"als_m3_cell_nbank_cf_orgnum",
|
||||
"als_m3_cell_nbank_com_orgnum",
|
||||
"als_m3_cell_nbank_oth_orgnum",
|
||||
"als_m3_cell_nbank_nsloan_orgnum",
|
||||
"als_m3_cell_nbank_autofin_orgnum",
|
||||
"als_m3_cell_nbank_sloan_orgnum",
|
||||
"als_m3_cell_nbank_cons_orgnum",
|
||||
"als_m3_cell_nbank_finlea_orgnum",
|
||||
"als_m3_cell_nbank_else_orgnum",
|
||||
"als_m3_cell_nbank_tot_mons",
|
||||
"als_m3_cell_nbank_avg_monnum",
|
||||
"als_m3_cell_nbank_max_monnum",
|
||||
"als_m3_cell_nbank_min_monnum",
|
||||
"als_m3_cell_nbank_max_inteday",
|
||||
"als_m3_cell_nbank_min_inteday",
|
||||
"als_m3_cell_nbank_week_allnum",
|
||||
"als_m3_cell_nbank_week_orgnum",
|
||||
"als_m3_cell_nbank_night_allnum",
|
||||
"als_m3_cell_nbank_night_orgnum",
|
||||
"als_m6_id_max_inteday",
|
||||
"als_m6_id_min_inteday",
|
||||
"als_m6_id_tot_mons",
|
||||
"als_m6_id_avg_monnum",
|
||||
"als_m6_id_max_monnum",
|
||||
"als_m6_id_min_monnum",
|
||||
"als_m6_id_pdl_allnum",
|
||||
"als_m6_id_pdl_orgnum",
|
||||
"als_m6_id_caon_allnum",
|
||||
"als_m6_id_caon_orgnum",
|
||||
"als_m6_id_rel_allnum",
|
||||
"als_m6_id_rel_orgnum",
|
||||
"als_m6_id_caoff_allnum",
|
||||
"als_m6_id_caoff_orgnum",
|
||||
"als_m6_id_cooff_allnum",
|
||||
"als_m6_id_cooff_orgnum",
|
||||
"als_m6_id_af_allnum",
|
||||
"als_m6_id_af_orgnum",
|
||||
"als_m6_id_coon_allnum",
|
||||
"als_m6_id_coon_orgnum",
|
||||
"als_m6_id_oth_allnum",
|
||||
"als_m6_id_oth_orgnum",
|
||||
"als_m6_id_bank_selfnum",
|
||||
"als_m6_id_bank_allnum",
|
||||
"als_m6_id_bank_tra_allnum",
|
||||
"als_m6_id_bank_ret_allnum",
|
||||
"als_m6_id_bank_orgnum",
|
||||
"als_m6_id_bank_tra_orgnum",
|
||||
"als_m6_id_bank_ret_orgnum",
|
||||
"als_m6_id_bank_tot_mons",
|
||||
"als_m6_id_bank_avg_monnum",
|
||||
"als_m6_id_bank_max_monnum",
|
||||
"als_m6_id_bank_min_monnum",
|
||||
"als_m6_id_bank_max_inteday",
|
||||
"als_m6_id_bank_min_inteday",
|
||||
"als_m6_id_bank_week_allnum",
|
||||
"als_m6_id_bank_week_orgnum",
|
||||
"als_m6_id_bank_night_allnum",
|
||||
"als_m6_id_bank_night_orgnum",
|
||||
"als_m6_id_nbank_selfnum",
|
||||
"als_m6_id_nbank_allnum",
|
||||
"als_m6_id_nbank_p2p_allnum",
|
||||
"als_m6_id_nbank_mc_allnum",
|
||||
"als_m6_id_nbank_ca_allnum",
|
||||
"als_m6_id_nbank_cf_allnum",
|
||||
"als_m6_id_nbank_com_allnum",
|
||||
"als_m6_id_nbank_oth_allnum",
|
||||
"als_m6_id_nbank_nsloan_allnum",
|
||||
"als_m6_id_nbank_autofin_allnum",
|
||||
"als_m6_id_nbank_sloan_allnum",
|
||||
"als_m6_id_nbank_cons_allnum",
|
||||
"als_m6_id_nbank_finlea_allnum",
|
||||
"als_m6_id_nbank_else_allnum",
|
||||
"als_m6_id_nbank_orgnum",
|
||||
"als_m6_id_nbank_p2p_orgnum",
|
||||
"als_m6_id_nbank_mc_orgnum",
|
||||
"als_m6_id_nbank_ca_orgnum",
|
||||
"als_m6_id_nbank_cf_orgnum",
|
||||
"als_m6_id_nbank_com_orgnum",
|
||||
"als_m6_id_nbank_oth_orgnum",
|
||||
"als_m6_id_nbank_nsloan_orgnum",
|
||||
"als_m6_id_nbank_autofin_orgnum",
|
||||
"als_m6_id_nbank_sloan_orgnum",
|
||||
"als_m6_id_nbank_cons_orgnum",
|
||||
"als_m6_id_nbank_finlea_orgnum",
|
||||
"als_m6_id_nbank_else_orgnum",
|
||||
"als_m6_id_nbank_tot_mons",
|
||||
"als_m6_id_nbank_avg_monnum",
|
||||
"als_m6_id_nbank_max_monnum",
|
||||
"als_m6_id_nbank_min_monnum",
|
||||
"als_m6_id_nbank_max_inteday",
|
||||
"als_m6_id_nbank_min_inteday",
|
||||
"als_m6_id_nbank_week_allnum",
|
||||
"als_m6_id_nbank_week_orgnum",
|
||||
"als_m6_id_nbank_night_allnum",
|
||||
"als_m6_id_nbank_night_orgnum",
|
||||
"als_m6_cell_max_inteday",
|
||||
"als_m6_cell_min_inteday",
|
||||
"als_m6_cell_tot_mons",
|
||||
"als_m6_cell_avg_monnum",
|
||||
"als_m6_cell_max_monnum",
|
||||
"als_m6_cell_min_monnum",
|
||||
"als_m6_cell_pdl_allnum",
|
||||
"als_m6_cell_pdl_orgnum",
|
||||
"als_m6_cell_caon_allnum",
|
||||
"als_m6_cell_caon_orgnum",
|
||||
"als_m6_cell_rel_allnum",
|
||||
"als_m6_cell_rel_orgnum",
|
||||
"als_m6_cell_caoff_allnum",
|
||||
"als_m6_cell_caoff_orgnum",
|
||||
"als_m6_cell_cooff_allnum",
|
||||
"als_m6_cell_cooff_orgnum",
|
||||
"als_m6_cell_af_allnum",
|
||||
"als_m6_cell_af_orgnum",
|
||||
"als_m6_cell_coon_allnum",
|
||||
"als_m6_cell_coon_orgnum",
|
||||
"als_m6_cell_oth_allnum",
|
||||
"als_m6_cell_oth_orgnum",
|
||||
"als_m6_cell_bank_selfnum",
|
||||
"als_m6_cell_bank_allnum",
|
||||
"als_m6_cell_bank_tra_allnum",
|
||||
"als_m6_cell_bank_ret_allnum",
|
||||
"als_m6_cell_bank_orgnum",
|
||||
"als_m6_cell_bank_tra_orgnum",
|
||||
"als_m6_cell_bank_ret_orgnum",
|
||||
"als_m6_cell_bank_tot_mons",
|
||||
"als_m6_cell_bank_avg_monnum",
|
||||
"als_m6_cell_bank_max_monnum",
|
||||
"als_m6_cell_bank_min_monnum",
|
||||
"als_m6_cell_bank_max_inteday",
|
||||
"als_m6_cell_bank_min_inteday",
|
||||
"als_m6_cell_bank_week_allnum",
|
||||
"als_m6_cell_bank_week_orgnum",
|
||||
"als_m6_cell_bank_night_allnum",
|
||||
"als_m6_cell_bank_night_orgnum",
|
||||
"als_m6_cell_nbank_selfnum",
|
||||
"als_m6_cell_nbank_allnum",
|
||||
"als_m6_cell_nbank_p2p_allnum",
|
||||
"als_m6_cell_nbank_mc_allnum",
|
||||
"als_m6_cell_nbank_ca_allnum",
|
||||
"als_m6_cell_nbank_cf_allnum",
|
||||
"als_m6_cell_nbank_com_allnum",
|
||||
"als_m6_cell_nbank_oth_allnum",
|
||||
"als_m6_cell_nbank_nsloan_allnum",
|
||||
"als_m6_cell_nbank_autofin_allnum",
|
||||
"als_m6_cell_nbank_sloan_allnum",
|
||||
"als_m6_cell_nbank_cons_allnum",
|
||||
"als_m6_cell_nbank_finlea_allnum",
|
||||
"als_m6_cell_nbank_else_allnum",
|
||||
"als_m6_cell_nbank_orgnum",
|
||||
"als_m6_cell_nbank_p2p_orgnum",
|
||||
"als_m6_cell_nbank_mc_orgnum",
|
||||
"als_m6_cell_nbank_ca_orgnum",
|
||||
"als_m6_cell_nbank_cf_orgnum",
|
||||
"als_m6_cell_nbank_com_orgnum",
|
||||
"als_m6_cell_nbank_oth_orgnum",
|
||||
"als_m6_cell_nbank_nsloan_orgnum",
|
||||
"als_m6_cell_nbank_autofin_orgnum",
|
||||
"als_m6_cell_nbank_sloan_orgnum",
|
||||
"als_m6_cell_nbank_cons_orgnum",
|
||||
"als_m6_cell_nbank_finlea_orgnum",
|
||||
"als_m6_cell_nbank_else_orgnum",
|
||||
"als_m6_cell_nbank_tot_mons",
|
||||
"als_m6_cell_nbank_avg_monnum",
|
||||
"als_m6_cell_nbank_max_monnum",
|
||||
"als_m6_cell_nbank_min_monnum",
|
||||
"als_m6_cell_nbank_max_inteday",
|
||||
"als_m6_cell_nbank_min_inteday",
|
||||
"als_m6_cell_nbank_week_allnum",
|
||||
"als_m6_cell_nbank_week_orgnum",
|
||||
"als_m6_cell_nbank_night_allnum",
|
||||
"als_m6_cell_nbank_night_orgnum",
|
||||
"als_m12_id_max_inteday",
|
||||
"als_m12_id_min_inteday",
|
||||
"als_m12_id_tot_mons",
|
||||
"als_m12_id_avg_monnum",
|
||||
"als_m12_id_max_monnum",
|
||||
"als_m12_id_min_monnum",
|
||||
"als_m12_id_pdl_allnum",
|
||||
"als_m12_id_pdl_orgnum",
|
||||
"als_m12_id_caon_allnum",
|
||||
"als_m12_id_caon_orgnum",
|
||||
"als_m12_id_rel_allnum",
|
||||
"als_m12_id_rel_orgnum",
|
||||
"als_m12_id_caoff_allnum",
|
||||
"als_m12_id_caoff_orgnum",
|
||||
"als_m12_id_cooff_allnum",
|
||||
"als_m12_id_cooff_orgnum",
|
||||
"als_m12_id_af_allnum",
|
||||
"als_m12_id_af_orgnum",
|
||||
"als_m12_id_coon_allnum",
|
||||
"als_m12_id_coon_orgnum",
|
||||
"als_m12_id_oth_allnum",
|
||||
"als_m12_id_oth_orgnum",
|
||||
"als_m12_id_bank_selfnum",
|
||||
"als_m12_id_bank_allnum",
|
||||
"als_m12_id_bank_tra_allnum",
|
||||
"als_m12_id_bank_ret_allnum",
|
||||
"als_m12_id_bank_orgnum",
|
||||
"als_m12_id_bank_tra_orgnum",
|
||||
"als_m12_id_bank_ret_orgnum",
|
||||
"als_m12_id_bank_tot_mons",
|
||||
"als_m12_id_bank_avg_monnum",
|
||||
"als_m12_id_bank_max_monnum",
|
||||
"als_m12_id_bank_min_monnum",
|
||||
"als_m12_id_bank_max_inteday",
|
||||
"als_m12_id_bank_min_inteday",
|
||||
"als_m12_id_bank_week_allnum",
|
||||
"als_m12_id_bank_week_orgnum",
|
||||
"als_m12_id_bank_night_allnum",
|
||||
"als_m12_id_bank_night_orgnum",
|
||||
"als_m12_id_nbank_selfnum",
|
||||
"als_m12_id_nbank_allnum",
|
||||
"als_m12_id_nbank_p2p_allnum",
|
||||
"als_m12_id_nbank_mc_allnum",
|
||||
"als_m12_id_nbank_ca_allnum",
|
||||
"als_m12_id_nbank_cf_allnum",
|
||||
"als_m12_id_nbank_com_allnum",
|
||||
"als_m12_id_nbank_oth_allnum",
|
||||
"als_m12_id_nbank_nsloan_allnum",
|
||||
"als_m12_id_nbank_autofin_allnum",
|
||||
"als_m12_id_nbank_sloan_allnum",
|
||||
"als_m12_id_nbank_cons_allnum",
|
||||
"als_m12_id_nbank_finlea_allnum",
|
||||
"als_m12_id_nbank_else_allnum",
|
||||
"als_m12_id_nbank_orgnum",
|
||||
"als_m12_id_nbank_p2p_orgnum",
|
||||
"als_m12_id_nbank_mc_orgnum",
|
||||
"als_m12_id_nbank_ca_orgnum",
|
||||
"als_m12_id_nbank_cf_orgnum",
|
||||
"als_m12_id_nbank_com_orgnum",
|
||||
"als_m12_id_nbank_oth_orgnum",
|
||||
"als_m12_id_nbank_nsloan_orgnum",
|
||||
"als_m12_id_nbank_autofin_orgnum",
|
||||
"als_m12_id_nbank_sloan_orgnum",
|
||||
"als_m12_id_nbank_cons_orgnum",
|
||||
"als_m12_id_nbank_finlea_orgnum",
|
||||
"als_m12_id_nbank_else_orgnum",
|
||||
"als_m12_id_nbank_tot_mons",
|
||||
"als_m12_id_nbank_avg_monnum",
|
||||
"als_m12_id_nbank_max_monnum",
|
||||
"als_m12_id_nbank_min_monnum",
|
||||
"als_m12_id_nbank_max_inteday",
|
||||
"als_m12_id_nbank_min_inteday",
|
||||
"als_m12_id_nbank_week_allnum",
|
||||
"als_m12_id_nbank_week_orgnum",
|
||||
"als_m12_id_nbank_night_allnum",
|
||||
"als_m12_id_nbank_night_orgnum",
|
||||
"als_m12_cell_max_inteday",
|
||||
"als_m12_cell_min_inteday",
|
||||
"als_m12_cell_tot_mons",
|
||||
"als_m12_cell_avg_monnum",
|
||||
"als_m12_cell_max_monnum",
|
||||
"als_m12_cell_min_monnum",
|
||||
"als_m12_cell_pdl_allnum",
|
||||
"als_m12_cell_pdl_orgnum",
|
||||
"als_m12_cell_caon_allnum",
|
||||
"als_m12_cell_caon_orgnum",
|
||||
"als_m12_cell_rel_allnum",
|
||||
"als_m12_cell_rel_orgnum",
|
||||
"als_m12_cell_caoff_allnum",
|
||||
"als_m12_cell_caoff_orgnum",
|
||||
"als_m12_cell_cooff_allnum",
|
||||
"als_m12_cell_cooff_orgnum",
|
||||
"als_m12_cell_af_allnum",
|
||||
"als_m12_cell_af_orgnum",
|
||||
"als_m12_cell_coon_allnum",
|
||||
"als_m12_cell_coon_orgnum",
|
||||
"als_m12_cell_oth_allnum",
|
||||
"als_m12_cell_oth_orgnum",
|
||||
"als_m12_cell_bank_selfnum",
|
||||
"als_m12_cell_bank_allnum",
|
||||
"als_m12_cell_bank_tra_allnum",
|
||||
"als_m12_cell_bank_ret_allnum",
|
||||
"als_m12_cell_bank_orgnum",
|
||||
"als_m12_cell_bank_tra_orgnum",
|
||||
"als_m12_cell_bank_ret_orgnum",
|
||||
"als_m12_cell_bank_tot_mons",
|
||||
"als_m12_cell_bank_avg_monnum",
|
||||
"als_m12_cell_bank_max_monnum",
|
||||
"als_m12_cell_bank_min_monnum",
|
||||
"als_m12_cell_bank_max_inteday",
|
||||
"als_m12_cell_bank_min_inteday",
|
||||
"als_m12_cell_bank_week_allnum",
|
||||
"als_m12_cell_bank_week_orgnum",
|
||||
"als_m12_cell_bank_night_allnum",
|
||||
"als_m12_cell_bank_night_orgnum",
|
||||
"als_m12_cell_nbank_selfnum",
|
||||
"als_m12_cell_nbank_allnum",
|
||||
"als_m12_cell_nbank_p2p_allnum",
|
||||
"als_m12_cell_nbank_mc_allnum",
|
||||
"als_m12_cell_nbank_ca_allnum",
|
||||
"als_m12_cell_nbank_cf_allnum",
|
||||
"als_m12_cell_nbank_com_allnum",
|
||||
"als_m12_cell_nbank_oth_allnum",
|
||||
"als_m12_cell_nbank_nsloan_allnum",
|
||||
"als_m12_cell_nbank_autofin_allnum",
|
||||
"als_m12_cell_nbank_sloan_allnum",
|
||||
"als_m12_cell_nbank_cons_allnum",
|
||||
"als_m12_cell_nbank_finlea_allnum",
|
||||
"als_m12_cell_nbank_else_allnum",
|
||||
"als_m12_cell_nbank_orgnum",
|
||||
"als_m12_cell_nbank_p2p_orgnum",
|
||||
"als_m12_cell_nbank_mc_orgnum",
|
||||
"als_m12_cell_nbank_ca_orgnum",
|
||||
"als_m12_cell_nbank_cf_orgnum",
|
||||
"als_m12_cell_nbank_com_orgnum",
|
||||
"als_m12_cell_nbank_oth_orgnum",
|
||||
"als_m12_cell_nbank_nsloan_orgnum",
|
||||
"als_m12_cell_nbank_autofin_orgnum",
|
||||
"als_m12_cell_nbank_sloan_orgnum",
|
||||
"als_m12_cell_nbank_cons_orgnum",
|
||||
"als_m12_cell_nbank_finlea_orgnum",
|
||||
"als_m12_cell_nbank_else_orgnum",
|
||||
"als_m12_cell_nbank_tot_mons",
|
||||
"als_m12_cell_nbank_avg_monnum",
|
||||
"als_m12_cell_nbank_max_monnum",
|
||||
"als_m12_cell_nbank_min_monnum",
|
||||
"als_m12_cell_nbank_max_inteday",
|
||||
"als_m12_cell_nbank_min_inteday",
|
||||
"als_m12_cell_nbank_week_allnum",
|
||||
"als_m12_cell_nbank_week_orgnum",
|
||||
"als_m12_cell_nbank_night_allnum",
|
||||
"als_m12_cell_nbank_night_orgnum",
|
||||
"als_fst_id_bank_inteday",
|
||||
"als_fst_id_nbank_inteday",
|
||||
"als_fst_cell_bank_inteday",
|
||||
"als_fst_cell_nbank_inteday",
|
||||
"als_lst_id_bank_inteday",
|
||||
"als_lst_id_bank_consnum",
|
||||
"als_lst_id_bank_csinteday",
|
||||
"als_lst_id_nbank_inteday",
|
||||
"als_lst_id_nbank_consnum",
|
||||
"als_lst_id_nbank_csinteday",
|
||||
"als_lst_cell_bank_inteday",
|
||||
"als_lst_cell_bank_consnum",
|
||||
"als_lst_cell_bank_csinteday",
|
||||
"als_lst_cell_nbank_inteday",
|
||||
"als_lst_cell_nbank_consnum",
|
||||
"als_lst_cell_nbank_csinteday",
|
||||
}
|
||||
|
||||
var jrzq6F2AKeySet = func() map[string]struct{} {
|
||||
m := make(map[string]struct{}, len(jrzq6F2AVariableKeys))
|
||||
for _, key := range jrzq6F2AVariableKeys {
|
||||
m[key] = struct{}{}
|
||||
}
|
||||
return m
|
||||
}()
|
||||
@@ -4,10 +4,11 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/xingwei"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
)
|
||||
|
||||
// ProcessJRZQ6F2ARequest JRZQ6F2A API处理方法 - 借贷申请记录
|
||||
@@ -21,27 +22,196 @@ func ProcessJRZQ6F2ARequest(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建请求数据,将项目规范的字段名转换为 XingweiService 需要的字段名
|
||||
reqData := map[string]interface{}{
|
||||
"name": paramsDto.Name,
|
||||
"idCardNum": paramsDto.IDCard,
|
||||
"phoneNumber": paramsDto.MobileNo,
|
||||
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
// 调用行为数据API,使用指定的project_id
|
||||
projectID := "CDJ-1101695369065984000"
|
||||
respBytes, err := deps.XingweiService.CallAPI(ctx, projectID, reqData)
|
||||
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
|
||||
if err != nil {
|
||||
if errors.Is(err, xingwei.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
} else if errors.Is(err, xingwei.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
encryptedMobileNo, err := deps.ZhichaService.Encrypt(paramsDto.MobileNo)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"name": encryptedName,
|
||||
"idCard": encryptedIDCard,
|
||||
"phone": encryptedMobileNo,
|
||||
"authorized": "1",
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI017", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, zhicha.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else if errors.Is(err, xingwei.ErrSystem) {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
respMap, ok := respData.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, errors.Join(processors.ErrSystem, errors.New("响应格式错误"))
|
||||
}
|
||||
|
||||
result := mapJRZQ3C7BToJRZQ6F2A(respMap)
|
||||
|
||||
respBytes, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
|
||||
func mapJRZQ3C7BToJRZQ6F2A(src map[string]interface{}) map[string]interface{} {
|
||||
variableValue := buildDefaultVariableValue()
|
||||
|
||||
// 如果源已经是平铺字段,优先直接覆盖,兼容不同返回形态。
|
||||
copyDirectFlattenFields(variableValue, src)
|
||||
|
||||
periods := []string{"d7", "d15", "m1", "m3", "m6", "m12"}
|
||||
for _, period := range periods {
|
||||
periodData := asMap(src[period])
|
||||
if len(periodData) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, scope := range []string{"id", "cell"} {
|
||||
scopeData := asMap(periodData[scope])
|
||||
if len(scopeData) == 0 {
|
||||
continue
|
||||
}
|
||||
flattenPeriodScope(variableValue, period, scope, scopeData)
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"risk_screen_v2": map[string]interface{}{
|
||||
"fulinHitFlag": 1,
|
||||
"models": []interface{}{},
|
||||
"variables": []interface{}{map[string]interface{}{"variableName": "bairong_applyloan_extend", "variableValue": variableValue}},
|
||||
"code": "OK",
|
||||
"decision": "accept",
|
||||
"propertyValidations": []interface{}{},
|
||||
"strategies": []interface{}{},
|
||||
"scenes": []interface{}{},
|
||||
"validateInfo": map[string]interface{}{"productCodes": []interface{}{}},
|
||||
"id": "",
|
||||
"message": "业务处理成功!",
|
||||
"knowledge": map[string]interface{}{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func flattenPeriodScope(target map[string]interface{}, period, scope string, scopeData map[string]interface{}) {
|
||||
basePrefix := "als_" + period + "_" + scope + "_"
|
||||
|
||||
// 先处理 scope 级基础字段(例如 tot_mons/max_monnum/min_monnum/avg_monnum)
|
||||
copyScalarFields(target, basePrefix, scopeData)
|
||||
|
||||
for key, raw := range scopeData {
|
||||
child := asMap(raw)
|
||||
if len(child) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
sectionPrefix := basePrefix + key + "_"
|
||||
copyScalarFields(target, sectionPrefix, child)
|
||||
|
||||
// 对周末字段做兼容命名映射
|
||||
copyAliasIfPresent(target, sectionPrefix, child, "weekend_allnum", "week_allnum")
|
||||
copyAliasIfPresent(target, sectionPrefix, child, "weekend_orgnum", "week_orgnum")
|
||||
|
||||
// 对 top_* 与 *_d 字段做兜底映射,尽可能补齐常用 allnum/orgnum
|
||||
if _, ok := target[sectionPrefix+"allnum"]; !ok {
|
||||
copyAliasIfPresent(target, sectionPrefix, child, "top_allnum", "allnum")
|
||||
copyAliasIfPresent(target, sectionPrefix, child, "allnum_d", "allnum")
|
||||
}
|
||||
if _, ok := target[sectionPrefix+"orgnum"]; !ok {
|
||||
copyAliasIfPresent(target, sectionPrefix, child, "top_orgnum", "orgnum")
|
||||
copyAliasIfPresent(target, sectionPrefix, child, "orgnum_d", "orgnum")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func copyScalarFields(target map[string]interface{}, prefix string, src map[string]interface{}) {
|
||||
for k, v := range src {
|
||||
if isScalar(v) {
|
||||
setVariableField(target, prefix+normalizeMetricName(k), v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func copyAliasIfPresent(target map[string]interface{}, prefix string, src map[string]interface{}, from, to string) {
|
||||
if v, ok := src[from]; ok && isScalar(v) {
|
||||
setVariableField(target, prefix+to, v)
|
||||
}
|
||||
}
|
||||
|
||||
func copyDirectFlattenFields(target map[string]interface{}, src map[string]interface{}) {
|
||||
for k, v := range src {
|
||||
if !isScalar(v) {
|
||||
continue
|
||||
}
|
||||
// 允许直接覆盖文档字段以及兼容字段
|
||||
setVariableField(target, k, v)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeMetricName(name string) string {
|
||||
switch name {
|
||||
case "weekend_allnum":
|
||||
return "week_allnum"
|
||||
case "weekend_orgnum":
|
||||
return "week_orgnum"
|
||||
default:
|
||||
return strings.TrimSpace(name)
|
||||
}
|
||||
}
|
||||
|
||||
func isScalar(v interface{}) bool {
|
||||
switch v.(type) {
|
||||
case nil:
|
||||
return false
|
||||
case string, bool, float64, int, int32, int64, uint, uint32, uint64:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func asMap(v interface{}) map[string]interface{} {
|
||||
if v == nil {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
if m, ok := v.(map[string]interface{}); ok {
|
||||
return m
|
||||
}
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
|
||||
func buildDefaultVariableValue() map[string]interface{} {
|
||||
m := make(map[string]interface{}, len(jrzq6F2AVariableKeys)+3)
|
||||
for _, key := range jrzq6F2AVariableKeys {
|
||||
m[key] = ""
|
||||
}
|
||||
|
||||
// 兼容历史示例中出现的附加字段
|
||||
m["als_Flag_applyloanstr"] = "1"
|
||||
m["code"] = "00"
|
||||
m["swift_number"] = ""
|
||||
m["flag_applyloanstr"] = "1"
|
||||
return m
|
||||
}
|
||||
|
||||
func setVariableField(target map[string]interface{}, key string, value interface{}) {
|
||||
_, inDoc := jrzq6F2AKeySet[key]
|
||||
if inDoc || key == "als_Flag_applyloanstr" || key == "code" || key == "swift_number" {
|
||||
target[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,13 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/xingwei"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
)
|
||||
|
||||
// ProcessJRZQ8B3CRequest JRZQ8B3C API处理方法 - 个人消费能力等级
|
||||
@@ -21,27 +24,173 @@ func ProcessJRZQ8B3CRequest(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建请求数据,将项目规范的字段名转换为 XingweiService 需要的字段名
|
||||
reqData := map[string]interface{}{
|
||||
"name": paramsDto.Name,
|
||||
"idCardNum": paramsDto.IDCard,
|
||||
"phoneNumber": paramsDto.MobileNo,
|
||||
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
// 调用行为数据API,使用指定的project_id
|
||||
projectID := "CDJ-1101695392528920576"
|
||||
respBytes, err := deps.XingweiService.CallAPI(ctx, projectID, reqData)
|
||||
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
|
||||
if err != nil {
|
||||
if errors.Is(err, xingwei.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
} else if errors.Is(err, xingwei.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
encryptedMobileNo, err := deps.ZhichaService.Encrypt(paramsDto.MobileNo)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"name": encryptedName,
|
||||
"idCard": encryptedIDCard,
|
||||
"phone": encryptedMobileNo,
|
||||
"authorized": "1",
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI034", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, zhicha.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else if errors.Is(err, xingwei.ErrSystem) {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
personIncomeIndex := "-1"
|
||||
if m, ok := respData.(map[string]interface{}); ok {
|
||||
personIncomeIndex = mapTap010ToIncomeIndex(m["tap010"], paramsDto.IDCard)
|
||||
}
|
||||
|
||||
respPayload := map[string]interface{}{
|
||||
"personincome_index_2.0": personIncomeIndex,
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(respPayload)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
|
||||
type incomeTier struct {
|
||||
Score int
|
||||
Low float64
|
||||
High float64 // 上界闭区间;math.Inf(1) 表示正无穷
|
||||
}
|
||||
|
||||
var incomeTiers = []incomeTier{
|
||||
{Score: 100, Low: 1000, High: 2000},
|
||||
{Score: 200, Low: 2000, High: 4000},
|
||||
{Score: 300, Low: 4000, High: 6000},
|
||||
{Score: 400, Low: 6000, High: 8000},
|
||||
{Score: 500, Low: 8000, High: 10000},
|
||||
{Score: 600, Low: 10000, High: 12000},
|
||||
{Score: 700, Low: 12000, High: 15000},
|
||||
{Score: 800, Low: 15000, High: 20000},
|
||||
{Score: 900, Low: 20000, High: 25000},
|
||||
{Score: 1000, Low: 25000, High: math.Inf(1)},
|
||||
}
|
||||
|
||||
func mapTap010ToIncomeIndex(rawTap010 interface{}, idCard string) string {
|
||||
tap010, ok := parseTap010Level(rawTap010)
|
||||
if !ok {
|
||||
return "-1"
|
||||
}
|
||||
|
||||
mappedLow, mappedHigh := expandTap010Range(tap010)
|
||||
candidateScores := intersectedTierScores(mappedLow, mappedHigh)
|
||||
if len(candidateScores) == 0 {
|
||||
return "-1"
|
||||
}
|
||||
|
||||
seed := stableSeedFromIDCard(idCard)
|
||||
score := candidateScores[seed%len(candidateScores)]
|
||||
return strconv.Itoa(score)
|
||||
}
|
||||
|
||||
func parseTap010Level(v interface{}) (int, bool) {
|
||||
switch value := v.(type) {
|
||||
case string:
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return 0, false
|
||||
}
|
||||
n, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
if n < 1 || n > 4 {
|
||||
return 0, false
|
||||
}
|
||||
return n, true
|
||||
case float64:
|
||||
n := int(value)
|
||||
if value != float64(n) || n < 1 || n > 4 {
|
||||
return 0, false
|
||||
}
|
||||
return n, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func expandTap010Range(level int) (float64, float64) {
|
||||
// tap010 原区间:
|
||||
// 1:(0,500) 2:[500,1000) 3:[1000,3000) 4:[3000,+inf)
|
||||
// 按比例放大 9 倍映射到收入尺度,满足示例: (0,500)->(0,4500)
|
||||
switch level {
|
||||
case 1:
|
||||
return 0, 4500
|
||||
case 2:
|
||||
return 4500, 9000
|
||||
case 3:
|
||||
return 9000, 27000
|
||||
case 4:
|
||||
return 27000, math.Inf(1)
|
||||
default:
|
||||
return 0, 0
|
||||
}
|
||||
}
|
||||
|
||||
func intersectedTierScores(low, high float64) []int {
|
||||
scores := make([]int, 0, len(incomeTiers))
|
||||
for _, t := range incomeTiers {
|
||||
if isRangeIntersect(low, high, t.Low, t.High) {
|
||||
scores = append(scores, t.Score)
|
||||
}
|
||||
}
|
||||
return scores
|
||||
}
|
||||
|
||||
func isRangeIntersect(aLow, aHigh, bLow, bHigh float64) bool {
|
||||
return aLow <= bHigh && bLow <= aHigh
|
||||
}
|
||||
|
||||
func stableSeedFromIDCard(idCard string) int {
|
||||
if len(idCard) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
runes := []rune(idCard)
|
||||
start := len(runes) - 4
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
|
||||
seed := 0
|
||||
for _, r := range runes[start:] {
|
||||
switch {
|
||||
case r >= '0' && r <= '9':
|
||||
seed = seed*11 + int(r-'0')
|
||||
case r == 'X' || r == 'x':
|
||||
seed = seed*11 + 10
|
||||
default:
|
||||
seed = seed*11 + int(r)%11
|
||||
}
|
||||
}
|
||||
|
||||
if seed < 0 {
|
||||
return -seed
|
||||
}
|
||||
return seed
|
||||
}
|
||||
|
||||
@@ -47,8 +47,6 @@ func ProcessJRZQO7L1Request(ctx context.Context, params []byte, deps *processors
|
||||
"city": null,
|
||||
}
|
||||
|
||||
// 使用 WithSkipCode201Check 不跳过 201 错误检查,当 Code == "201" 时返回错误
|
||||
// ctx = zhicha.WithSkipCode201Check(ctx)
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI080", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, zhicha.ErrDatasource) {
|
||||
|
||||
@@ -75,6 +75,5 @@ func ProcessQCXGGB2QRequest(ctx context.Context, params []byte, deps *processors
|
||||
}
|
||||
}
|
||||
|
||||
// 极光服务已经返回了 data 字段的 JSON,直接返回即可
|
||||
return respBytes, nil
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ func ProcessQYGL2S0WRequest(ctx context.Context, params []byte, deps *processors
|
||||
fmt.Print("个人身份证件号不能为空")
|
||||
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, errors.New("当失信被执行人类型为个人时,身份证件号不能为空"))
|
||||
}
|
||||
if paramsDto.IDCard == "410482198504029333" {
|
||||
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
|
||||
}
|
||||
} else if paramsDto.Type == "ent" {
|
||||
// 企业查询:name 和 entMark 两者必填其一
|
||||
nameValue = paramsDto.EntName
|
||||
|
||||
@@ -4,13 +4,14 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessQYGL6S1BRequest QYGL6S1B API处理方法 - 董监高司法综合信息核验
|
||||
// ProcessQYGL6S1BRequest QYGL6S1B API处理方法 - 董监高司法综合信息核验(使用数据宝服务)
|
||||
|
||||
func ProcessQYGL6S1BRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QYGL6S1BReq
|
||||
@@ -22,31 +23,106 @@ func ProcessQYGL6S1BRequest(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
encryptedIdCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
|
||||
// 构建数据宝入参
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "1cce582f0a6f3ca40de80f1bea9b9698",
|
||||
"idcard": paramsDto.IDCard,
|
||||
}
|
||||
|
||||
// 调用数据宝API
|
||||
apiPath := "/communication/personal/10166"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
// 解析响应中的 JSON 字符串(使用 RecursiveParse)
|
||||
parsedResp, err := RecursiveParse(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
// 构建API调用参数
|
||||
reqData := map[string]interface{}{
|
||||
"idCard": encryptedIdCard,
|
||||
"authorized": paramsDto.Authorized,
|
||||
// 提取 resultData 字段
|
||||
resultData, ok := parsedResp.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, errors.Join(processors.ErrSystem, errors.New("invalid response format"))
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI043", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, zhicha.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
resultDataValue, exists := resultData["resultData"]
|
||||
if !exists {
|
||||
// 如果 resultData 不存在,说明查询为空,返回空的业务数据结构
|
||||
emptyResult := map[string]interface{}{
|
||||
"caseInfoList": []interface{}{},
|
||||
"legRepInfoList": []interface{}{},
|
||||
"lossPromiseList": []interface{}{},
|
||||
"performerList": []interface{}{},
|
||||
"ryPosPerList": []interface{}{},
|
||||
"shareholderList": []interface{}{},
|
||||
}
|
||||
return json.Marshal(emptyResult)
|
||||
}
|
||||
|
||||
// 将响应数据转换为JSON字节
|
||||
respBytes, err := json.Marshal(respData)
|
||||
// 转换数据类型:将数字字段转换为字符串
|
||||
convertedData, err := convertDataTypes(resultDataValue)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(convertedData)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
|
||||
// convertDataTypes 递归转换数据类型,将数字字段转换为字符串以保持与原有格式一致
|
||||
func convertDataTypes(data interface{}) (interface{}, error) {
|
||||
switch v := data.(type) {
|
||||
case map[string]interface{}:
|
||||
for key, val := range v {
|
||||
converted, err := convertDataTypes(val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v[key] = converted
|
||||
}
|
||||
return v, nil
|
||||
case []interface{}:
|
||||
for i, item := range v {
|
||||
converted, err := convertDataTypes(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v[i] = converted
|
||||
}
|
||||
return v, nil
|
||||
case float64:
|
||||
// 将 float64 类型转换为字符串(JSON 解析后数字默认为 float64)
|
||||
if v == float64(int64(v)) {
|
||||
return strconv.FormatInt(int64(v), 10), nil
|
||||
}
|
||||
return strconv.FormatFloat(v, 'f', -1, 64), nil
|
||||
case int:
|
||||
return strconv.Itoa(v), nil
|
||||
case int32:
|
||||
return strconv.FormatInt(int64(v), 10), nil
|
||||
case int64:
|
||||
return strconv.FormatInt(v, 10), nil
|
||||
case string:
|
||||
// 尝试解析字符串中的 JSON
|
||||
var parsed interface{}
|
||||
if err := json.Unmarshal([]byte(v), &parsed); err == nil {
|
||||
return convertDataTypes(parsed)
|
||||
}
|
||||
return v, nil
|
||||
default:
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package qygl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessQYGL8848Request QYGL8848 企业税收违法核查 API 处理方法(使用数据宝服务示例)
|
||||
func ProcessQYGL8848Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QYGLDJ12Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 企业名称(entName)、统一社会信用代码(creditCode)、企业注册号(entRegNo) 至少传其一;多填时优先用 creditCode 传参
|
||||
hasEntName := paramsDto.EntName != ""
|
||||
hasEntCode := paramsDto.EntCode != ""
|
||||
hasEntRegNo := paramsDto.EntRegNo != ""
|
||||
if !hasEntName && !hasEntCode && !hasEntRegNo { // 三个都未填才报错
|
||||
return nil, errors.Join(processors.ErrInvalidParam, errors.New("ent_name、ent_code、ent_reg_no 至少需要传其中一个"))
|
||||
}
|
||||
|
||||
// 构建数据宝入参(多填时优先取 creditCode)
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "c67673dd2e92deb2d2ec91b87bb0a81c",
|
||||
}
|
||||
if hasEntCode {
|
||||
reqParams["creditCode"] = paramsDto.EntCode
|
||||
} else if hasEntName {
|
||||
reqParams["entName"] = paramsDto.EntName
|
||||
} else if hasEntRegNo {
|
||||
reqParams["regCode"] = paramsDto.EntRegNo
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/communication/personal/10233"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
// 解析响应中的 JSON 字符串(使用 qyglb4c0 中的 RecursiveParse)
|
||||
parsedResp, err := RecursiveParse(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(parsedResp)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package qygl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessQYGLDJ12Request QYGLDJ12 企业年报信息核验 API 处理方法(使用数据宝服务示例)
|
||||
func ProcessQYGLDJ12Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QYGLDJ12Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 企业名称(entName)、统一社会信用代码(creditCode)、企业注册号(entRegNo) 至少传其一;多填时优先用 creditCode 传参
|
||||
hasEntName := paramsDto.EntName != ""
|
||||
hasEntCode := paramsDto.EntCode != ""
|
||||
hasEntRegNo := paramsDto.EntRegNo != ""
|
||||
if !hasEntName && !hasEntCode && !hasEntRegNo { // 三个都未填才报错
|
||||
return nil, errors.Join(processors.ErrInvalidParam, errors.New("ent_name、ent_code、ent_reg_no 至少需要传其中一个"))
|
||||
}
|
||||
|
||||
// 构建数据宝入参(sign 外的业务参数可按需 AES 加密后作为 bodyData)
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "112813815e2cc281ad8f552deb7a3c7f",
|
||||
}
|
||||
if hasEntCode {
|
||||
reqParams["creditCode"] = paramsDto.EntCode
|
||||
} else if hasEntName {
|
||||
reqParams["entName"] = paramsDto.EntName
|
||||
} else if hasEntRegNo {
|
||||
reqParams["regCode"] = paramsDto.EntRegNo
|
||||
}
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/communication/personal/10192"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
// 解析响应中的 JSON 字符串(使用 qyglb4c0 中的 RecursiveParse)
|
||||
parsedResp, err := RecursiveParse(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(parsedResp)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package qygl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessQYGLDJ33Request QYGLDJ33 企业进出口信用核查 API 处理方法(使用数据宝服务示例)
|
||||
func ProcessQYGLDJ33Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QYGLDJ33Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 企业名称(entName)、统一社会信用代码(creditCode)、企业注册号(entRegNo) 至少传其一;多填时优先用 creditCode 传参
|
||||
hasEntName := paramsDto.EntName != ""
|
||||
hasEntCode := paramsDto.EntCode != ""
|
||||
hasEntRegNo := paramsDto.EntRegNo != ""
|
||||
if !hasEntName && !hasEntCode && !hasEntRegNo { // 三个都未填才报错
|
||||
return nil, errors.Join(processors.ErrInvalidParam, errors.New("ent_name、ent_code、ent_reg_no 至少需要传其中一个"))
|
||||
}
|
||||
|
||||
// 构建数据宝入参(sign 外的业务参数可按需 AES 加密后作为 bodyData)
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "f51ed30b0d4208bf7e6f2ba499d49d4f",
|
||||
}
|
||||
if hasEntCode {
|
||||
reqParams["creditCode"] = paramsDto.EntCode
|
||||
} else if hasEntName {
|
||||
reqParams["entName"] = paramsDto.EntName
|
||||
} else if hasEntRegNo {
|
||||
reqParams["regCode"] = paramsDto.EntRegNo
|
||||
}
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/communication/personal/10254"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
// 解析响应中的 JSON 字符串(使用 qyglb4c0 中的 RecursiveParse)
|
||||
parsedResp, err := RecursiveParse(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(parsedResp)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -16,11 +17,12 @@ import (
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
)
|
||||
|
||||
// ProcessQYGLJ1U9Request 企业全景报告处理器:并发调用企业全量(QYGLUY3S)、股权全景(QYGLJ0Q1)、司法涉诉(QYGL5S1I),
|
||||
// 然后复用 qyglj1u9_processor_build.go 中的 buildReport / map* 逻辑生成企业报告结构
|
||||
// ProcessQYGLJ1U9Request 企业全景报告处理器:并发调用企业全量(QYGLUY3S)、股权全景(QYGLJ0Q1)、司法涉诉(QYGL5S1I)、
|
||||
// 企业年报(QYGLDJ12)、税收违法(QYGL8848)、欠税公告(QYGL7D9A)。
|
||||
// 单路失败、查无、解析失败时该路按空数据处理并继续合并;仅当合并后的报告仍无任何可展示的企业要素时返回查询为空。
|
||||
func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
// 复用 QYGLUY3S 的入参结构:企业名称/注册号/统一社会信用代码
|
||||
var p dto.QYGLUY3SReq
|
||||
var p dto.QYGLJ1U9Req
|
||||
if err := json.Unmarshal(params, &p); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
@@ -28,13 +30,13 @@ func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 并发调用三个已有处理器
|
||||
// 并发调用六个子处理器;单路失败或无数据时降级为空结果,仅当合并后仍无任何企业要素时返回查询为空
|
||||
type apiResult struct {
|
||||
key string
|
||||
data map[string]interface{}
|
||||
err error
|
||||
}
|
||||
resultsCh := make(chan apiResult, 3)
|
||||
resultsCh := make(chan apiResult, 6)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
call := func(key string, req interface{}, fn func(context.Context, []byte, *processors.ProcessorDependencies) ([]byte, error)) {
|
||||
@@ -52,8 +54,15 @@ func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors
|
||||
return
|
||||
}
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(resp, &m); err != nil {
|
||||
resultsCh <- apiResult{key: key, err: err}
|
||||
var uerr error
|
||||
// 根节点可能是数组或非对象,与欠税接口一致用宽松解析
|
||||
if key == "taxArrears" || key == "annualReport" || key == "taxViolation" {
|
||||
m, uerr = unmarshalToReportMap(resp)
|
||||
} else {
|
||||
uerr = json.Unmarshal(resp, &m)
|
||||
}
|
||||
if uerr != nil {
|
||||
resultsCh <- apiResult{key: key, err: uerr}
|
||||
return
|
||||
}
|
||||
resultsCh <- apiResult{key: key, data: m}
|
||||
@@ -62,15 +71,13 @@ func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors
|
||||
|
||||
// 企业全量信息核验V2(QYGLUY3S)
|
||||
call("jiguangFull", map[string]interface{}{
|
||||
"ent_name": p.EntName,
|
||||
"ent_reg_no": p.EntRegno,
|
||||
"ent_code": p.EntCode,
|
||||
"ent_name": p.EntName,
|
||||
"ent_code": p.EntCode,
|
||||
}, ProcessQYGLUY3SRequest)
|
||||
|
||||
// 企业股权结构全景(QYGLJ0Q1)
|
||||
call("equityPanorama", map[string]interface{}{
|
||||
"ent_name": p.EntName,
|
||||
"ent_code": p.EntCode,
|
||||
}, ProcessQYGLJ0Q1Request)
|
||||
|
||||
// 企业司法涉诉V2(QYGL5S1I)
|
||||
@@ -79,14 +86,37 @@ func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors
|
||||
"ent_code": p.EntCode,
|
||||
}, ProcessQYGL5S1IRequest)
|
||||
|
||||
// 企业年报信息核验(QYGLDJ12)
|
||||
call("annualReport", map[string]interface{}{
|
||||
"ent_name": p.EntName,
|
||||
"ent_code": p.EntCode,
|
||||
}, ProcessQYGLDJ12Request)
|
||||
|
||||
// 企业税收违法核查(QYGL8848)
|
||||
call("taxViolation", map[string]interface{}{
|
||||
"ent_name": p.EntName,
|
||||
"ent_code": p.EntCode,
|
||||
}, ProcessQYGL8848Request)
|
||||
|
||||
// 欠税公告(QYGL7D9A,天眼查 OwnTax,keyword 为统一社会信用代码)
|
||||
call("taxArrears", map[string]interface{}{
|
||||
"ent_code": p.EntCode,
|
||||
"page_size": 20,
|
||||
"page_num": 1,
|
||||
}, ProcessQYGL7D9ARequest)
|
||||
|
||||
wg.Wait()
|
||||
close(resultsCh)
|
||||
|
||||
var jiguang, judicial, equity map[string]interface{}
|
||||
jiguang := map[string]interface{}{}
|
||||
judicial := map[string]interface{}{}
|
||||
equity := map[string]interface{}{}
|
||||
annualReport := map[string]interface{}{}
|
||||
taxViolation := map[string]interface{}{}
|
||||
taxArrears := map[string]interface{}{}
|
||||
for r := range resultsCh {
|
||||
if r.err != nil {
|
||||
// 任一关键数据源异常,则返回系统错误(也可以根据需求做降级)
|
||||
return nil, errors.Join(processors.ErrSystem, fmt.Errorf("%s 调用失败: %w", r.key, r.err))
|
||||
if r.err != nil || r.data == nil {
|
||||
continue
|
||||
}
|
||||
switch r.key {
|
||||
case "jiguangFull":
|
||||
@@ -95,25 +125,30 @@ func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors
|
||||
judicial = r.data
|
||||
case "equityPanorama":
|
||||
equity = r.data
|
||||
case "annualReport":
|
||||
annualReport = r.data
|
||||
case "taxViolation":
|
||||
taxViolation = r.data
|
||||
case "taxArrears":
|
||||
taxArrears = r.data
|
||||
}
|
||||
}
|
||||
if jiguang == nil {
|
||||
jiguang = map[string]interface{}{}
|
||||
}
|
||||
if judicial == nil {
|
||||
judicial = map[string]interface{}{}
|
||||
}
|
||||
if equity == nil {
|
||||
equity = map[string]interface{}{}
|
||||
}
|
||||
|
||||
// 复用构建逻辑生成企业报告结构
|
||||
report := buildReport(jiguang, judicial, equity)
|
||||
// 复用构建逻辑生成企业报告结构(含年报 / 税收违法 / 欠税公告的转化结果)
|
||||
report := buildReport(jiguang, judicial, equity, annualReport, taxViolation, taxArrears)
|
||||
if !qyglJ1U9ReportHasSubstantiveData(report) {
|
||||
return nil, errors.Join(processors.ErrNotFound, errors.New("未查询到可用于生成报告的企业数据"))
|
||||
}
|
||||
|
||||
// 为报告生成唯一编号并缓存,供后续通过编号查看
|
||||
reportID := saveQYGLReport(report)
|
||||
report["reportId"] = reportID
|
||||
|
||||
// 异步预生成 PDF(写入磁盘缓存),用户点击「保存为 PDF」时可直读缓存
|
||||
if deps.ReportPDFScheduler != nil {
|
||||
deps.ReportPDFScheduler.ScheduleQYGLReportPDF(context.Background(), reportID)
|
||||
}
|
||||
|
||||
// 持久化企业报告记录到数据库(忽略持久化失败,不影响接口主流程)
|
||||
if deps.ReportRepo != nil {
|
||||
reqJSON, _ := json.Marshal(p)
|
||||
@@ -129,7 +164,7 @@ func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors
|
||||
})
|
||||
}
|
||||
// 为报告补充前端查看链接,供调用方直接跳转到企业报告页面(通过编号访问)
|
||||
report["reportUrl"] = buildQYGLReportURLByID(reportID)
|
||||
report["reportUrl"] = buildQYGLReportURLByID(deps.APIPublicBaseURL, reportID)
|
||||
|
||||
out, err := json.Marshal(report)
|
||||
if err != nil {
|
||||
@@ -138,6 +173,18 @@ func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// unmarshalToReportMap 将 JSON 解析为报告用 map;根节点非对象时包在 data 下(兼容欠税等接口根为数组的情况)。
|
||||
func unmarshalToReportMap(b []byte) (map[string]interface{}, error) {
|
||||
var raw interface{}
|
||||
if err := json.Unmarshal(b, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if m, ok := raw.(map[string]interface{}); ok {
|
||||
return m, nil
|
||||
}
|
||||
return map[string]interface{}{"data": raw}, nil
|
||||
}
|
||||
|
||||
// 内存中的企业报告缓存(简单实现,进程重启后清空)
|
||||
var qyglReportStore = struct {
|
||||
sync.RWMutex
|
||||
@@ -173,8 +220,12 @@ func generateQYGLReportID() string {
|
||||
return fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// buildQYGLReportURLByID 构造企业报告前端查看链接(通过编号查看)
|
||||
func buildQYGLReportURLByID(id string) string {
|
||||
return "/reports/qygl/" + url.PathEscape(id)
|
||||
// buildQYGLReportURLByID 构造企业报告前端查看链接(通过编号查看)。
|
||||
// publicBase 为对外 API 基址(如 https://api.example.com),空则返回站内相对路径。
|
||||
func buildQYGLReportURLByID(publicBase, id string) string {
|
||||
path := "/reports/qygl/" + url.PathEscape(id)
|
||||
if publicBase == "" {
|
||||
return path
|
||||
}
|
||||
return strings.TrimRight(publicBase, "/") + path
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,17 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func buildReport(jiguang, judicial, equity map[string]interface{}) map[string]interface{} {
|
||||
func buildReport(jiguang, judicial, equity, annualRaw, taxViolationRaw, taxArrearsRaw map[string]interface{}) map[string]interface{} {
|
||||
report := make(map[string]interface{})
|
||||
report["reportTime"] = time.Now().Format("2006-01-02 15:04:05")
|
||||
|
||||
// 先转化年报接口数据;若有内容则以 QYGLDJ12 为准,不再使用全量 V2 中 YEARREPORT* 表(避免与「企业年报」板块重复)
|
||||
annualReports := mapAnnualReports(annualRaw)
|
||||
jgNoYearReport := jiguang
|
||||
if len(annualReports) > 0 {
|
||||
jgNoYearReport = jiguangWithoutYearReportTables(jiguang)
|
||||
}
|
||||
|
||||
basic := mapFromBASIC(jiguang)
|
||||
report["creditCode"] = str(basic["creditCode"])
|
||||
report["entName"] = str(basic["entName"])
|
||||
@@ -20,21 +27,22 @@ func buildReport(jiguang, judicial, equity map[string]interface{}) map[string]in
|
||||
report["branches"] = mapBranches(jiguang)
|
||||
// 股权/实控人/受益人/对外投资:有股权全景时以其为准,否则用全量信息
|
||||
if len(equity) > 0 {
|
||||
report["shareholding"] = mapShareholdingWithEquity(jiguang, equity)
|
||||
report["shareholding"] = mapShareholdingWithEquity(jgNoYearReport, equity)
|
||||
report["controller"] = mapControllerFromEquity(equity)
|
||||
report["beneficiaries"] = mapBeneficiariesFromEquity(equity)
|
||||
report["investments"] = mapInvestmentsWithEquity(jiguang, equity)
|
||||
} else {
|
||||
report["shareholding"] = mapShareholding(jiguang)
|
||||
report["shareholding"] = mapShareholding(jgNoYearReport)
|
||||
report["controller"] = mapController(jiguang)
|
||||
report["beneficiaries"] = mapBeneficiaries()
|
||||
report["investments"] = mapInvestments(jiguang)
|
||||
}
|
||||
report["guarantees"] = mapGuarantees(jiguang)
|
||||
report["management"] = mapManagement(jiguang)
|
||||
report["assets"] = mapAssets(jiguang)
|
||||
// 以下块在全量 V2 中依赖年报类表;接入 DJ12 后改从 jgNoYearReport 读取(已剔除 YEARREPORT*)
|
||||
report["guarantees"] = mapGuarantees(jgNoYearReport)
|
||||
report["management"] = mapManagement(jgNoYearReport)
|
||||
report["assets"] = mapAssets(jgNoYearReport)
|
||||
report["licenses"] = mapLicenses(jiguang)
|
||||
report["activities"] = mapActivities(jiguang)
|
||||
report["activities"] = mapActivities(jgNoYearReport)
|
||||
report["inspections"] = mapInspections(jiguang)
|
||||
risks := mapRisks(jiguang, judicial)
|
||||
report["risks"] = risks
|
||||
@@ -45,9 +53,860 @@ func buildReport(jiguang, judicial, equity map[string]interface{}) map[string]in
|
||||
if bl, _ := jiguang["BASICLIST"].([]interface{}); len(bl) > 0 {
|
||||
report["basicList"] = bl
|
||||
}
|
||||
|
||||
// QYGLDJ12 企业年报 / QYGL8848 税收违法 / QYGL7D9A 欠税公告(转化后的前端友好结构)
|
||||
report["annualReports"] = annualReports
|
||||
report["taxViolations"] = mapTaxViolations(taxViolationRaw)
|
||||
report["ownTaxNotices"] = mapOwnTaxNotices(taxArrearsRaw)
|
||||
applyQYGLJ1U9ReportFieldDefaults(report)
|
||||
return report
|
||||
}
|
||||
|
||||
// applyQYGLJ1U9ReportFieldDefaults 在子数据源缺失时仍保证报告字段齐全:字符串 ""、数值 0、数组 []、对象按约定填空结构(不向客户暴露「缺键」)。
|
||||
func applyQYGLJ1U9ReportFieldDefaults(report map[string]interface{}) {
|
||||
if report == nil {
|
||||
return
|
||||
}
|
||||
// 顶层字符串
|
||||
report["entName"] = str(report["entName"])
|
||||
report["creditCode"] = str(report["creditCode"])
|
||||
report["reportTime"] = str(report["reportTime"])
|
||||
|
||||
report["basic"] = mergeBasicDefaults(report["basic"])
|
||||
b, _ := report["basic"].(map[string]interface{})
|
||||
if str(report["entName"]) == "" {
|
||||
report["entName"] = str(b["entName"])
|
||||
}
|
||||
if str(report["creditCode"]) == "" {
|
||||
report["creditCode"] = str(b["creditCode"])
|
||||
}
|
||||
|
||||
report["branches"] = ensureSlice(report["branches"])
|
||||
report["guarantees"] = ensureSlice(report["guarantees"])
|
||||
report["inspections"] = ensureSlice(report["inspections"])
|
||||
report["timeline"] = ensureSlice(report["timeline"])
|
||||
report["beneficiaries"] = ensureSlice(report["beneficiaries"])
|
||||
report["annualReports"] = ensureSlice(report["annualReports"])
|
||||
if _, ok := report["basicList"]; !ok {
|
||||
report["basicList"] = []interface{}{}
|
||||
} else {
|
||||
report["basicList"] = ensureSlice(report["basicList"])
|
||||
}
|
||||
|
||||
report["shareholding"] = mergeShareholdingDefaults(report["shareholding"])
|
||||
report["controller"] = mergeControllerDefaults(report["controller"])
|
||||
report["investments"] = mergeInvestmentsDefaults(report["investments"])
|
||||
report["management"] = mergeManagementDefaults(report["management"])
|
||||
report["assets"] = mergeAssetsDefaults(report["assets"])
|
||||
report["licenses"] = mergeLicensesDefaults(report["licenses"])
|
||||
report["activities"] = mergeActivitiesDefaults(report["activities"])
|
||||
report["risks"] = mergeRisksDefaults(report["risks"])
|
||||
report["listed"] = mergeListedDefaults(report["listed"])
|
||||
|
||||
report["taxViolations"] = mergeTaxViolationsDefaults(report["taxViolations"])
|
||||
report["ownTaxNotices"] = mergeOwnTaxNoticesDefaults(report["ownTaxNotices"])
|
||||
|
||||
if ro, ok := report["riskOverview"].(map[string]interface{}); ok {
|
||||
report["riskOverview"] = mergeRiskOverviewDefaults(ro)
|
||||
} else {
|
||||
report["riskOverview"] = mergeRiskOverviewDefaults(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func mergeBasicDefaults(v interface{}) map[string]interface{} {
|
||||
out, _ := v.(map[string]interface{})
|
||||
if out == nil {
|
||||
out = map[string]interface{}{}
|
||||
}
|
||||
skel := map[string]interface{}{
|
||||
"entName": "",
|
||||
"creditCode": "",
|
||||
"regNo": "",
|
||||
"orgCode": "",
|
||||
"entType": "",
|
||||
"entTypeCode": "",
|
||||
"entityTypeCode": "",
|
||||
"establishDate": "",
|
||||
"registeredCapital": float64(0),
|
||||
"regCapCurrency": "",
|
||||
"regCapCurrencyCode": "",
|
||||
"regOrg": "",
|
||||
"regOrgCode": "",
|
||||
"regProvince": "",
|
||||
"provinceCode": "",
|
||||
"regCity": "",
|
||||
"regCityCode": "",
|
||||
"regDistrict": "",
|
||||
"districtCode": "",
|
||||
"address": "",
|
||||
"postalCode": "",
|
||||
"legalRepresentative": "",
|
||||
"compositionForm": "",
|
||||
"approvedBusinessItem": "",
|
||||
"status": "",
|
||||
"statusCode": "",
|
||||
"operationPeriodFrom": "",
|
||||
"operationPeriodTo": "",
|
||||
"approveDate": "",
|
||||
"cancelDate": "",
|
||||
"revokeDate": "",
|
||||
"cancelReason": "",
|
||||
"revokeReason": "",
|
||||
"businessScope": "",
|
||||
"lastAnnuReportYear": "",
|
||||
"oldNames": []interface{}{},
|
||||
}
|
||||
for k, def := range skel {
|
||||
if _, ok := out[k]; !ok {
|
||||
out[k] = def
|
||||
}
|
||||
}
|
||||
if out["oldNames"] == nil {
|
||||
out["oldNames"] = []interface{}{}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func ensureSlice(v interface{}) []interface{} {
|
||||
if v == nil {
|
||||
return []interface{}{}
|
||||
}
|
||||
arr, ok := v.([]interface{})
|
||||
if !ok {
|
||||
return []interface{}{}
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
func mergeShareholdingDefaults(v interface{}) map[string]interface{} {
|
||||
out, _ := v.(map[string]interface{})
|
||||
if out == nil {
|
||||
out = map[string]interface{}{}
|
||||
}
|
||||
skel := map[string]interface{}{
|
||||
"shareholders": []interface{}{},
|
||||
"equityChanges": []interface{}{},
|
||||
"equityPledges": []interface{}{},
|
||||
"paidInDetails": []interface{}{},
|
||||
"subscribedCapitalDetails": []interface{}{},
|
||||
"hasEquityPledges": false,
|
||||
"shareholderCount": 0,
|
||||
"registeredCapital": float64(0),
|
||||
"currency": "",
|
||||
"topHolderName": "",
|
||||
"topHolderPercent": float64(0),
|
||||
"top5TotalPercent": float64(0),
|
||||
}
|
||||
for k, def := range skel {
|
||||
if _, ok := out[k]; !ok {
|
||||
out[k] = def
|
||||
}
|
||||
}
|
||||
for _, k := range []string{"shareholders", "equityChanges", "equityPledges", "paidInDetails", "subscribedCapitalDetails"} {
|
||||
out[k] = ensureSlice(out[k])
|
||||
}
|
||||
if out["registeredCapital"] == nil {
|
||||
out["registeredCapital"] = float64(0)
|
||||
}
|
||||
if _, ok := out["hasEquityPledges"].(bool); !ok {
|
||||
out["hasEquityPledges"] = false
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mergeControllerDefaults(v interface{}) map[string]interface{} {
|
||||
if m, ok := v.(map[string]interface{}); ok && m != nil {
|
||||
if m["path"] == nil {
|
||||
m["path"] = map[string]interface{}{
|
||||
"nodes": []interface{}{},
|
||||
"links": []interface{}{},
|
||||
}
|
||||
}
|
||||
if p, ok := m["path"].(map[string]interface{}); ok {
|
||||
if _, ok := p["nodes"]; !ok {
|
||||
p["nodes"] = []interface{}{}
|
||||
}
|
||||
if _, ok := p["links"]; !ok {
|
||||
p["links"] = []interface{}{}
|
||||
}
|
||||
}
|
||||
ensureStrKeys(m, []string{"id", "name", "type", "reason", "source"})
|
||||
if m["percent"] == nil {
|
||||
m["percent"] = float64(0)
|
||||
} else if _, ok := m["percent"].(float64); !ok {
|
||||
m["percent"] = num(m["percent"])
|
||||
}
|
||||
return m
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"id": "",
|
||||
"name": "",
|
||||
"type": "",
|
||||
"percent": float64(0),
|
||||
"path": map[string]interface{}{
|
||||
"nodes": []interface{}{},
|
||||
"links": []interface{}{},
|
||||
},
|
||||
"reason": "",
|
||||
"source": "",
|
||||
}
|
||||
}
|
||||
|
||||
func mergeInvestmentsDefaults(v interface{}) map[string]interface{} {
|
||||
out, _ := v.(map[string]interface{})
|
||||
if out == nil {
|
||||
out = map[string]interface{}{}
|
||||
}
|
||||
if _, ok := out["totalCount"]; !ok {
|
||||
out["totalCount"] = 0
|
||||
}
|
||||
if _, ok := out["totalAmount"]; !ok {
|
||||
out["totalAmount"] = float64(0)
|
||||
}
|
||||
if out["totalAmount"] == nil {
|
||||
out["totalAmount"] = float64(0)
|
||||
}
|
||||
out["list"] = ensureSlice(out["list"])
|
||||
out["legalRepresentativeInvestments"] = ensureSlice(out["legalRepresentativeInvestments"])
|
||||
return out
|
||||
}
|
||||
|
||||
func mergeManagementDefaults(v interface{}) map[string]interface{} {
|
||||
out, _ := v.(map[string]interface{})
|
||||
if out == nil {
|
||||
out = map[string]interface{}{}
|
||||
}
|
||||
if _, ok := out["executives"]; !ok {
|
||||
out["executives"] = []interface{}{}
|
||||
} else {
|
||||
out["executives"] = ensureSlice(out["executives"])
|
||||
}
|
||||
if _, ok := out["legalRepresentativeOtherPositions"]; !ok {
|
||||
out["legalRepresentativeOtherPositions"] = []interface{}{}
|
||||
} else {
|
||||
out["legalRepresentativeOtherPositions"] = ensureSlice(out["legalRepresentativeOtherPositions"])
|
||||
}
|
||||
if _, ok := out["employeeCount"]; !ok || out["employeeCount"] == nil {
|
||||
out["employeeCount"] = float64(0)
|
||||
}
|
||||
if _, ok := out["femaleEmployeeCount"]; !ok || out["femaleEmployeeCount"] == nil {
|
||||
out["femaleEmployeeCount"] = float64(0)
|
||||
}
|
||||
if out["socialSecurity"] == nil {
|
||||
out["socialSecurity"] = map[string]interface{}{}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mergeAssetsDefaults(v interface{}) map[string]interface{} {
|
||||
out, _ := v.(map[string]interface{})
|
||||
if out == nil {
|
||||
out = map[string]interface{}{}
|
||||
}
|
||||
if _, ok := out["years"]; !ok {
|
||||
out["years"] = []interface{}{}
|
||||
} else {
|
||||
out["years"] = ensureSlice(out["years"])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mergeLicensesDefaults(v interface{}) map[string]interface{} {
|
||||
out, _ := v.(map[string]interface{})
|
||||
if out == nil {
|
||||
out = map[string]interface{}{}
|
||||
}
|
||||
for _, k := range []string{"permits", "permitChanges", "ipPledges", "otherLicenses"} {
|
||||
if _, ok := out[k]; !ok {
|
||||
out[k] = []interface{}{}
|
||||
} else {
|
||||
out[k] = ensureSlice(out[k])
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mergeActivitiesDefaults(v interface{}) map[string]interface{} {
|
||||
out, _ := v.(map[string]interface{})
|
||||
if out == nil {
|
||||
out = map[string]interface{}{}
|
||||
}
|
||||
if _, ok := out["bids"]; !ok {
|
||||
out["bids"] = []interface{}{}
|
||||
} else {
|
||||
out["bids"] = ensureSlice(out["bids"])
|
||||
}
|
||||
if _, ok := out["websites"]; !ok {
|
||||
out["websites"] = []interface{}{}
|
||||
} else {
|
||||
out["websites"] = ensureSlice(out["websites"])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func litigationTypeKeys() []string {
|
||||
return []string{
|
||||
"administrative", "implement", "preservation", "civil", "criminal",
|
||||
"bankrupt", "jurisdict", "compensate",
|
||||
}
|
||||
}
|
||||
|
||||
func defaultLitigationShell() map[string]interface{} {
|
||||
out := map[string]interface{}{"totalCases": 0}
|
||||
for _, k := range litigationTypeKeys() {
|
||||
out[k] = map[string]interface{}{
|
||||
"count": 0,
|
||||
"cases": []interface{}{},
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mergeLitigationShape(v interface{}) map[string]interface{} {
|
||||
out := defaultLitigationShell()
|
||||
if v == nil {
|
||||
return out
|
||||
}
|
||||
m, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
return out
|
||||
}
|
||||
known := map[string]struct{}{}
|
||||
for _, k := range litigationTypeKeys() {
|
||||
known[k] = struct{}{}
|
||||
}
|
||||
for k, val := range m {
|
||||
if k == "totalCases" {
|
||||
out["totalCases"] = intFromAny(val)
|
||||
continue
|
||||
}
|
||||
if _, isCat := known[k]; isCat {
|
||||
sm, ok := val.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out[k] = map[string]interface{}{
|
||||
"count": intFromAny(sm["count"]),
|
||||
"cases": ensureSlice(sm["cases"]),
|
||||
}
|
||||
continue
|
||||
}
|
||||
out[k] = val
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func defaultQuickCancelShell() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"entName": "",
|
||||
"creditCode": "",
|
||||
"regNo": "",
|
||||
"regOrg": "",
|
||||
"noticeFromDate": "",
|
||||
"noticeToDate": "",
|
||||
"cancelResult": "",
|
||||
"dissents": []interface{}{},
|
||||
}
|
||||
}
|
||||
|
||||
func defaultLiquidationShell() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"principal": "",
|
||||
"members": []interface{}{},
|
||||
}
|
||||
}
|
||||
|
||||
func mergeRisksDefaults(v interface{}) map[string]interface{} {
|
||||
out, _ := v.(map[string]interface{})
|
||||
if out == nil {
|
||||
out = map[string]interface{}{}
|
||||
}
|
||||
boolKeys := []string{
|
||||
"hasCourtJudgments", "hasJudicialAssists", "hasDishonestDebtors", "hasLimitHighDebtors",
|
||||
"hasAdminPenalty", "hasException", "hasSeriousIllegal", "hasTaxOwing", "hasSeriousTaxIllegal",
|
||||
"hasMortgage", "hasEquityPledges", "hasQuickCancel",
|
||||
}
|
||||
for _, k := range boolKeys {
|
||||
if _, ok := out[k]; !ok {
|
||||
out[k] = false
|
||||
}
|
||||
}
|
||||
if _, ok := out["riskLevel"]; !ok {
|
||||
out["riskLevel"] = "低"
|
||||
}
|
||||
if _, ok := out["riskScore"]; !ok {
|
||||
out["riskScore"] = 80
|
||||
}
|
||||
for _, k := range []string{"dishonestDebtorCount", "limitHighDebtorCount"} {
|
||||
if _, ok := out[k]; !ok {
|
||||
out[k] = 0
|
||||
}
|
||||
}
|
||||
for _, k := range []string{
|
||||
"courtJudgments", "judicialAssists", "dishonestDebtors", "limitHighDebtors",
|
||||
"adminPenalties", "adminPenaltyUpdates", "exceptions", "seriousIllegals", "mortgages",
|
||||
} {
|
||||
if _, ok := out[k]; !ok {
|
||||
out[k] = []interface{}{}
|
||||
} else {
|
||||
out[k] = ensureSlice(out[k])
|
||||
}
|
||||
}
|
||||
out["litigation"] = mergeLitigationShape(out["litigation"])
|
||||
if out["quickCancel"] == nil {
|
||||
out["quickCancel"] = defaultQuickCancelShell()
|
||||
} else if qm, ok := out["quickCancel"].(map[string]interface{}); ok {
|
||||
dc := defaultQuickCancelShell()
|
||||
for k, def := range dc {
|
||||
if _, ok := qm[k]; !ok {
|
||||
qm[k] = def
|
||||
}
|
||||
}
|
||||
if qm["dissents"] == nil {
|
||||
qm["dissents"] = []interface{}{}
|
||||
} else {
|
||||
qm["dissents"] = ensureSlice(qm["dissents"])
|
||||
}
|
||||
out["quickCancel"] = qm
|
||||
}
|
||||
if out["liquidation"] == nil {
|
||||
out["liquidation"] = defaultLiquidationShell()
|
||||
} else if lm, ok := out["liquidation"].(map[string]interface{}); ok {
|
||||
dc := defaultLiquidationShell()
|
||||
for k, def := range dc {
|
||||
if _, ok := lm[k]; !ok {
|
||||
lm[k] = def
|
||||
}
|
||||
}
|
||||
if lm["members"] == nil {
|
||||
lm["members"] = []interface{}{}
|
||||
} else {
|
||||
lm["members"] = ensureSlice(lm["members"])
|
||||
}
|
||||
out["liquidation"] = lm
|
||||
}
|
||||
tr, _ := out["taxRecords"].(map[string]interface{})
|
||||
if tr == nil {
|
||||
tr = map[string]interface{}{}
|
||||
}
|
||||
for _, k := range []string{"taxLevelAYears", "seriousTaxIllegal", "taxOwings"} {
|
||||
if _, ok := tr[k]; !ok {
|
||||
tr[k] = []interface{}{}
|
||||
} else {
|
||||
tr[k] = ensureSlice(tr[k])
|
||||
}
|
||||
}
|
||||
out["taxRecords"] = tr
|
||||
return out
|
||||
}
|
||||
|
||||
// normalizeListedStock 无股票结构时用 JSON null,避免前端把 {} 当成有值而 JSON.stringify 出 "{}"。
|
||||
func normalizeListedStock(v interface{}) interface{} {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
m, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
return v
|
||||
}
|
||||
if len(m) == 0 {
|
||||
return nil
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func mergeListedDefaults(v interface{}) map[string]interface{} {
|
||||
if m, ok := v.(map[string]interface{}); ok && m != nil {
|
||||
if _, ok := m["isListed"].(bool); !ok {
|
||||
m["isListed"] = false
|
||||
}
|
||||
co, _ := m["company"].(map[string]interface{})
|
||||
if co == nil {
|
||||
co = map[string]interface{}{}
|
||||
}
|
||||
for k, def := range map[string]interface{}{
|
||||
"bizScope": "", "creditCode": "", "regAddr": "", "regCapital": "",
|
||||
"orgCode": "", "cur": "", "curName": "",
|
||||
} {
|
||||
if _, ok := co[k]; !ok {
|
||||
co[k] = def
|
||||
}
|
||||
}
|
||||
m["company"] = co
|
||||
m["stock"] = normalizeListedStock(m["stock"])
|
||||
if _, ok := m["topShareholders"]; !ok {
|
||||
m["topShareholders"] = []interface{}{}
|
||||
} else {
|
||||
m["topShareholders"] = ensureSlice(m["topShareholders"])
|
||||
}
|
||||
if _, ok := m["listedManagers"]; !ok {
|
||||
m["listedManagers"] = []interface{}{}
|
||||
} else {
|
||||
m["listedManagers"] = ensureSlice(m["listedManagers"])
|
||||
}
|
||||
return m
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"isListed": false,
|
||||
"company": map[string]interface{}{
|
||||
"bizScope": "", "creditCode": "", "regAddr": "", "regCapital": "",
|
||||
"orgCode": "", "cur": "", "curName": "",
|
||||
},
|
||||
"stock": nil,
|
||||
"topShareholders": []interface{}{},
|
||||
"listedManagers": []interface{}{},
|
||||
}
|
||||
}
|
||||
|
||||
func mergeTaxViolationsDefaults(v interface{}) map[string]interface{} {
|
||||
out, _ := v.(map[string]interface{})
|
||||
if out == nil {
|
||||
out = map[string]interface{}{}
|
||||
}
|
||||
if _, ok := out["total"]; !ok {
|
||||
out["total"] = 0
|
||||
}
|
||||
if _, ok := out["items"]; !ok {
|
||||
out["items"] = []interface{}{}
|
||||
} else {
|
||||
out["items"] = ensureSlice(out["items"])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mergeOwnTaxNoticesDefaults(v interface{}) map[string]interface{} {
|
||||
return mergeTaxViolationsDefaults(v)
|
||||
}
|
||||
|
||||
func mergeRiskOverviewDefaults(v interface{}) map[string]interface{} {
|
||||
out, _ := v.(map[string]interface{})
|
||||
if out == nil {
|
||||
out = map[string]interface{}{}
|
||||
}
|
||||
if _, ok := out["riskLevel"]; !ok {
|
||||
out["riskLevel"] = "低"
|
||||
}
|
||||
if _, ok := out["riskScore"]; !ok {
|
||||
out["riskScore"] = 100
|
||||
}
|
||||
if _, ok := out["tags"]; !ok {
|
||||
out["tags"] = []interface{}{}
|
||||
} else {
|
||||
out["tags"] = ensureSlice(out["tags"])
|
||||
}
|
||||
if _, ok := out["items"]; !ok {
|
||||
out["items"] = []interface{}{}
|
||||
} else {
|
||||
out["items"] = ensureSlice(out["items"])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func ensureStrKeys(m map[string]interface{}, keys []string) {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
for _, k := range keys {
|
||||
if _, ok := m[k]; !ok {
|
||||
m[k] = ""
|
||||
} else if m[k] == nil {
|
||||
m[k] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// qyglJ1U9ReportHasSubstantiveData 判断合并后的报告是否至少含一项可展示的企业要素。
|
||||
// 当所有子数据源均失败或等价于无数据时返回 false,用于 QYGLJ1U9 整体返回「查询为空」。
|
||||
func qyglJ1U9ReportHasSubstantiveData(report map[string]interface{}) bool {
|
||||
if report == nil {
|
||||
return false
|
||||
}
|
||||
trim := func(v interface{}) string { return strings.TrimSpace(str(v)) }
|
||||
|
||||
basic, _ := report["basic"].(map[string]interface{})
|
||||
if basic != nil {
|
||||
if trim(basic["entName"]) != "" || trim(basic["creditCode"]) != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if trim(report["entName"]) != "" || trim(report["creditCode"]) != "" {
|
||||
return true
|
||||
}
|
||||
if ar, ok := report["annualReports"].([]interface{}); ok && len(ar) > 0 {
|
||||
return true
|
||||
}
|
||||
if tv, ok := report["taxViolations"].(map[string]interface{}); ok {
|
||||
if intFromAny(tv["total"]) > 0 {
|
||||
return true
|
||||
}
|
||||
if items, ok := tv["items"].([]interface{}); ok && len(items) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if ot, ok := report["ownTaxNotices"].(map[string]interface{}); ok {
|
||||
if intFromAny(ot["total"]) > 0 {
|
||||
return true
|
||||
}
|
||||
if items, ok := ot["items"].([]interface{}); ok && len(items) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if br, ok := report["branches"].([]interface{}); ok && len(br) > 0 {
|
||||
return true
|
||||
}
|
||||
if bl, ok := report["basicList"].([]interface{}); ok && len(bl) > 0 {
|
||||
return true
|
||||
}
|
||||
if sh, ok := report["shareholding"].(map[string]interface{}); ok {
|
||||
if arr, ok := sh["shareholders"].([]interface{}); ok && len(arr) > 0 {
|
||||
return true
|
||||
}
|
||||
if intFromAny(sh["shareholderCount"]) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if inv, ok := report["investments"].(map[string]interface{}); ok {
|
||||
if arr, ok := inv["list"].([]interface{}); ok && len(arr) > 0 {
|
||||
return true
|
||||
}
|
||||
if intFromAny(inv["totalCount"]) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if ben, ok := report["beneficiaries"].([]interface{}); ok && len(ben) > 0 {
|
||||
return true
|
||||
}
|
||||
if tl, ok := report["timeline"].([]interface{}); ok && len(tl) > 0 {
|
||||
return true
|
||||
}
|
||||
if ctl, _ := report["controller"].(map[string]interface{}); ctl != nil && trim(ctl["name"]) != "" {
|
||||
return true
|
||||
}
|
||||
if risks, ok := report["risks"].(map[string]interface{}); ok {
|
||||
for _, key := range []string{
|
||||
"dishonestDebtors", "limitHighDebtors", "adminPenalties", "exceptions",
|
||||
"seriousIllegals", "mortgages", "courtJudgments", "judicialAssists",
|
||||
} {
|
||||
if arr, ok := risks[key].([]interface{}); ok && len(arr) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if lit, ok := risks["litigation"].(map[string]interface{}); ok {
|
||||
for _, v := range lit {
|
||||
sub, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if arr, ok := sub["cases"].([]interface{}); ok && len(arr) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// jiguangWithoutYearReportTables 浅拷贝全量 map,并去掉企业全量 V2 中与「公示年报」对应的 YEARREPORT* 键。
|
||||
// 在已接入 QYGLDJ12 且年报列表非空时使用,避免 build 与 HTML 中与「十六、企业年报」重复展示。
|
||||
func jiguangWithoutYearReportTables(jiguang map[string]interface{}) map[string]interface{} {
|
||||
if len(jiguang) == 0 {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
strip := map[string]struct{}{
|
||||
"YEARREPORTFORGUARANTEE": {},
|
||||
"YEARREPORTPAIDUPCAPITAL": {},
|
||||
"YEARREPORTSUBCAPITAL": {},
|
||||
"YEARREPORTBASIC": {},
|
||||
"YEARREPORTSOCSEC": {},
|
||||
"YEARREPORTANASSETSINFO": {},
|
||||
"YEARREPORTWEBSITEINFO": {},
|
||||
}
|
||||
out := make(map[string]interface{}, len(jiguang))
|
||||
for k, v := range jiguang {
|
||||
if _, drop := strip[k]; drop {
|
||||
continue
|
||||
}
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// BuildReportFromRawSources 供开发/测试:将各处理器原始 JSON(与 QYGLJ1U9 并发结果形态一致)走与线上一致的 buildReport 转化。
|
||||
// 任一路传入 nil 时按空 map 处理。
|
||||
func BuildReportFromRawSources(jiguang, judicial, equity, annualRaw, taxViolationRaw, taxArrearsRaw map[string]interface{}) map[string]interface{} {
|
||||
if jiguang == nil {
|
||||
jiguang = map[string]interface{}{}
|
||||
}
|
||||
if judicial == nil {
|
||||
judicial = map[string]interface{}{}
|
||||
}
|
||||
if equity == nil {
|
||||
equity = map[string]interface{}{}
|
||||
}
|
||||
if annualRaw == nil {
|
||||
annualRaw = map[string]interface{}{}
|
||||
}
|
||||
if taxViolationRaw == nil {
|
||||
taxViolationRaw = map[string]interface{}{}
|
||||
}
|
||||
if taxArrearsRaw == nil {
|
||||
taxArrearsRaw = map[string]interface{}{}
|
||||
}
|
||||
return buildReport(jiguang, judicial, equity, annualRaw, taxViolationRaw, taxArrearsRaw)
|
||||
}
|
||||
|
||||
// extractJSONArrayFromEnterpriseAPI 从数据宝/天眼查类响应中提取数组主体(data/list/result 等)。
|
||||
func extractJSONArrayFromEnterpriseAPI(m map[string]interface{}) []interface{} {
|
||||
if len(m) == 0 {
|
||||
return nil
|
||||
}
|
||||
priority := []string{"data", "list", "result", "records", "items", "body"}
|
||||
for _, k := range priority {
|
||||
if arr := sliceOrEmpty(m[k]); len(arr) > 0 {
|
||||
return arr
|
||||
}
|
||||
}
|
||||
if len(m) == 1 {
|
||||
for _, v := range m {
|
||||
if arr, ok := v.([]interface{}); ok {
|
||||
return arr
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func intFromAny(v interface{}) int {
|
||||
if v == nil {
|
||||
return 0
|
||||
}
|
||||
switch x := v.(type) {
|
||||
case float64:
|
||||
return int(x)
|
||||
case int:
|
||||
return x
|
||||
case int64:
|
||||
return int(x)
|
||||
default:
|
||||
s := strings.TrimSpace(str(x))
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
if n, err := strconv.Atoi(s); err == nil {
|
||||
return n
|
||||
}
|
||||
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
||||
return int(f)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// mapOwnTaxNotices QYGL7D9A 欠税公告 → { total, items }
|
||||
func mapOwnTaxNotices(raw map[string]interface{}) map[string]interface{} {
|
||||
out := map[string]interface{}{
|
||||
"total": 0,
|
||||
"items": []interface{}{},
|
||||
}
|
||||
if raw == nil {
|
||||
return out
|
||||
}
|
||||
items := sliceOrEmpty(raw["items"])
|
||||
total := intFromAny(raw["total"])
|
||||
if total == 0 {
|
||||
total = len(items)
|
||||
}
|
||||
mapped := make([]interface{}, 0, len(items))
|
||||
for _, it := range items {
|
||||
row, _ := it.(map[string]interface{})
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
mapped = append(mapped, map[string]interface{}{
|
||||
"taxIdNumber": str(row["taxIdNumber"]),
|
||||
"taxpayerName": str(row["name"]),
|
||||
"taxCategory": str(row["taxCategory"]),
|
||||
"ownTaxBalance": str(row["ownTaxBalance"]),
|
||||
"ownTaxAmount": str(row["ownTaxAmount"]),
|
||||
"newOwnTaxBalance": str(row["newOwnTaxBalance"]),
|
||||
"taxType": str(row["type"]),
|
||||
"publishDate": str(row["publishDate"]),
|
||||
"department": str(row["department"]),
|
||||
"location": str(row["location"]),
|
||||
"legalPersonName": str(row["legalpersonName"]),
|
||||
"personIdNumber": str(row["personIdNumber"]),
|
||||
"personIdName": str(row["personIdName"]),
|
||||
"taxpayerType": str(row["taxpayerType"]),
|
||||
"regType": str(row["regType"]),
|
||||
})
|
||||
}
|
||||
out["total"] = total
|
||||
out["items"] = mapped
|
||||
return out
|
||||
}
|
||||
|
||||
// mapTaxViolations QYGL8848 税收违法 → { total, items }(字段驼峰化)
|
||||
func mapTaxViolations(raw map[string]interface{}) map[string]interface{} {
|
||||
out := map[string]interface{}{
|
||||
"total": 0,
|
||||
"items": []interface{}{},
|
||||
}
|
||||
if raw == nil {
|
||||
return out
|
||||
}
|
||||
items := sliceOrEmpty(raw["items"])
|
||||
total := intFromAny(raw["total"])
|
||||
if total == 0 {
|
||||
total = len(items)
|
||||
}
|
||||
mapped := make([]interface{}, 0, len(items))
|
||||
for _, it := range items {
|
||||
row, _ := it.(map[string]interface{})
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
cm := convertReportKeysToCamel(row, true)
|
||||
if mm, ok := cm.(map[string]interface{}); ok {
|
||||
mapped = append(mapped, mm)
|
||||
}
|
||||
}
|
||||
out["total"] = total
|
||||
out["items"] = mapped
|
||||
return out
|
||||
}
|
||||
|
||||
// mapAnnualReports QYGLDJ12 企业年报列表 → []年报对象(键名驼峰化,按 reportYear 降序)
|
||||
func mapAnnualReports(raw map[string]interface{}) []interface{} {
|
||||
rows := extractJSONArrayFromEnterpriseAPI(raw)
|
||||
if len(rows) == 0 {
|
||||
return []interface{}{}
|
||||
}
|
||||
out := make([]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
m, ok := r.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if v, ok := m["rpport_change_info"]; ok {
|
||||
if _, has := m["report_change_info"]; !has {
|
||||
m["report_change_info"] = v
|
||||
}
|
||||
}
|
||||
cm := convertReportKeysToCamel(m, true)
|
||||
mm, ok := cm.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out = append(out, mm)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
ai, _ := out[i].(map[string]interface{})
|
||||
bj, _ := out[j].(map[string]interface{})
|
||||
return intFromAny(ai["reportYear"]) > intFromAny(bj["reportYear"])
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func mapFromBASIC(jiguang map[string]interface{}) map[string]interface{} {
|
||||
basic := make(map[string]interface{})
|
||||
b, _ := jiguang["BASIC"].(map[string]interface{})
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package qygl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
sharedvalidator "tyapi-server/internal/shared/validator"
|
||||
)
|
||||
|
||||
// TestQYGLJ1U9Req_ValidateParams 仅验证 QYGLJ1U9 入参的校验规则(特别是 validUSCI)。
|
||||
func TestQYGLJ1U9Req_ValidateParams(t *testing.T) {
|
||||
// 使用全局业务校验器
|
||||
bv := sharedvalidator.NewBusinessValidator()
|
||||
|
||||
t.Run("invalid_usci_should_fail", func(t *testing.T) {
|
||||
req := dto.QYGLJ1U9Req{
|
||||
EntName: "测试企业有限公司",
|
||||
EntCode: "123", // 明显不符合 validUSCI
|
||||
}
|
||||
if err := bv.ValidateStruct(req); err == nil {
|
||||
t.Fatalf("expected validation error for invalid ent_code, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid_usci_should_pass", func(t *testing.T) {
|
||||
req := dto.QYGLJ1U9Req{
|
||||
EntName: "杭州娃哈哈集团有限公司",
|
||||
EntCode: "91330000142916567N", // 符合 validUSCI 正则的示例
|
||||
}
|
||||
if err := bv.ValidateStruct(req); err != nil {
|
||||
t.Fatalf("expected no validation error for valid ent_code, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package processors
|
||||
|
||||
import "context"
|
||||
|
||||
// QYGLReportPDFScheduler 企业全景报告 PDF 异步预生成调度器(可为 nil 表示禁用)
|
||||
type QYGLReportPDFScheduler interface {
|
||||
// ScheduleQYGLReportPDF 在报告数据就绪后异步生成 PDF 并写入缓存
|
||||
ScheduleQYGLReportPDF(ctx context.Context, reportID string)
|
||||
}
|
||||
@@ -8,21 +8,9 @@ import (
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// shumaiMobileThreeResp 数脉 /v4/mobile_three/check 返回的 data 结构
|
||||
// result: 0-一致 1-不一致 2-无记录;channel: cmcc/cucc/ctcc/gdcc
|
||||
type shumaiMobileThreeResp struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
Result string `json:"result"`
|
||||
Desc string `json:"desc"`
|
||||
Channel string `json:"channel"`
|
||||
Sex string `json:"sex"`
|
||||
Birthday string `json:"birthday"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
// YYSY09CDResponse 最终返回结构
|
||||
// code: 1000一致 1001不一致 1002查无
|
||||
type YYSY09CDResponse struct {
|
||||
@@ -47,43 +35,48 @@ func ProcessYYSY09CDRequest(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
reqFormData := map[string]interface{}{
|
||||
"idcard": paramsDto.IDCard,
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "c115708d915451da8f34a23e144dda6b",
|
||||
"name": paramsDto.Name,
|
||||
"idcard": paramsDto.IDCard,
|
||||
"mobile": paramsDto.MobileNo,
|
||||
}
|
||||
|
||||
apiPath := "/v4/mobile_three/check"
|
||||
|
||||
// 先尝试使用政务接口(app_id2 和 app_secret2)
|
||||
respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData, true)
|
||||
apiPath := "/communication/personal/1979"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
// 使用实时接口(app_id 和 app_secret)重试
|
||||
respBytes, err = deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData, false)
|
||||
// 如果重试后仍然失败,返回错误
|
||||
if err != nil {
|
||||
if errors.Is(err, shumai.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else if errors.Is(err, shumai.ErrSystem) {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
out, err := mapShumaiMobileThreeToYYSY09CD(respBytes)
|
||||
out, err := mapYYSYK9R4ToYYSY09CD(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
// mapShumaiMobileThreeToYYSY09CD 数脉 mobile_three/check 的 data -> 最终格式
|
||||
// result: 0->1000一致 1->1001不一致 2->1002查无 其它->1002查无;channel: cmcc->CMCC cucc->CUCC ctcc->CTCC gdcc->CBN
|
||||
func mapShumaiMobileThreeToYYSY09CD(dataBytes []byte) (*YYSY09CDResponse, error) {
|
||||
var r shumaiMobileThreeResp
|
||||
if err := json.Unmarshal(dataBytes, &r); err != nil {
|
||||
// yysyk9r4Resp 数据宝 YYSYK9R4 接口 data 结构
|
||||
// state: 1-验证一致 2-验证不一致 3-异常情况
|
||||
type yysyk9r4Resp struct {
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
// mapYYSYK9R4ToYYSY09CD 数据宝 YYSYK9R4 的 data -> YYSY09CD 最终格式
|
||||
// state: 1->1000一致 2->1001不一致 其它->1002查无
|
||||
func mapYYSYK9R4ToYYSY09CD(data interface{}) (*YYSY09CDResponse, error) {
|
||||
var r yysyk9r4Resp
|
||||
|
||||
b, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(b, &r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -91,44 +84,26 @@ func mapShumaiMobileThreeToYYSY09CD(dataBytes []byte) (*YYSY09CDResponse, error)
|
||||
var codeStr string
|
||||
var codeInt int
|
||||
var msg string
|
||||
switch strings.TrimSpace(r.Result) {
|
||||
case "0":
|
||||
switch strings.TrimSpace(r.State) {
|
||||
case "1":
|
||||
codeStr = "1000"
|
||||
codeInt = 1000
|
||||
msg = "一致"
|
||||
case "1":
|
||||
case "2":
|
||||
codeStr = "1001"
|
||||
codeInt = 1001
|
||||
msg = "不一致"
|
||||
case "2":
|
||||
codeStr = "1002"
|
||||
codeInt = 1002
|
||||
msg = "查无"
|
||||
default:
|
||||
codeStr = "1002"
|
||||
codeInt = 1002
|
||||
msg = "查无"
|
||||
}
|
||||
|
||||
// phoneType: cmcc->CMCC cucc->CUCC ctcc->CTCC gdcc->CBN(广电)
|
||||
ch := strings.ToLower(strings.TrimSpace(r.Channel))
|
||||
var phoneType string
|
||||
switch ch {
|
||||
case "cmcc":
|
||||
phoneType = "CMCC"
|
||||
case "cucc":
|
||||
phoneType = "CUCC"
|
||||
case "ctcc":
|
||||
phoneType = "CTCC"
|
||||
case "gdcc":
|
||||
phoneType = "CBN"
|
||||
}
|
||||
|
||||
return &YYSY09CDResponse{
|
||||
Code: codeStr,
|
||||
Data: YYSY09CDResponseData{
|
||||
Msg: msg,
|
||||
PhoneType: phoneType,
|
||||
PhoneType: "",
|
||||
Code: codeInt,
|
||||
EncryptType: "MD5",
|
||||
},
|
||||
|
||||
@@ -2,46 +2,44 @@ package yysy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/xingwei"
|
||||
)
|
||||
|
||||
// ProcessYYSY8C2DRequest YYSY8C2D API处理方法 - 运营商三要素查询
|
||||
func ProcessYYSY8C2DRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.YYSY8C2DReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return ProcessYYSY9A1BRequest(ctx, params, deps)
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
// var paramsDto dto.YYSY8C2DReq
|
||||
// if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
// return nil, errors.Join(processors.ErrSystem, err)
|
||||
// }
|
||||
|
||||
// 构建请求数据,将项目规范的字段名转换为 XingweiService 需要的字段名
|
||||
reqData := map[string]interface{}{
|
||||
"name": paramsDto.Name,
|
||||
"idCardNum": paramsDto.IDCard,
|
||||
"phoneNumber": paramsDto.MobileNo,
|
||||
}
|
||||
// if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
// return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
// }
|
||||
|
||||
// 调用行为数据API,使用指定的project_id
|
||||
projectID := "CDJ-1100244702166183936"
|
||||
respBytes, err := deps.XingweiService.CallAPI(ctx, projectID, reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, xingwei.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
} else if errors.Is(err, xingwei.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else if errors.Is(err, xingwei.ErrSystem) {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
// // 构建请求数据,将项目规范的字段名转换为 XingweiService 需要的字段名
|
||||
// reqData := map[string]interface{}{
|
||||
// "name": paramsDto.Name,
|
||||
// "idCardNum": paramsDto.IDCard,
|
||||
// "phoneNumber": paramsDto.MobileNo,
|
||||
// }
|
||||
|
||||
return respBytes, nil
|
||||
// // 调用行为数据API,使用指定的project_id
|
||||
// projectID := "CDJ-1100244702166183936"
|
||||
// respBytes, err := deps.XingweiService.CallAPI(ctx, projectID, reqData)
|
||||
// if err != nil {
|
||||
// if errors.Is(err, xingwei.ErrNotFound) {
|
||||
// return nil, errors.Join(processors.ErrNotFound, err)
|
||||
// } else if errors.Is(err, xingwei.ErrDatasource) {
|
||||
// return nil, errors.Join(processors.ErrDatasource, err)
|
||||
// } else if errors.Is(err, xingwei.ErrSystem) {
|
||||
// return nil, errors.Join(processors.ErrSystem, err)
|
||||
// } else {
|
||||
// return nil, errors.Join(processors.ErrSystem, err)
|
||||
// }
|
||||
// }
|
||||
|
||||
// return respBytes, nil
|
||||
}
|
||||
|
||||
@@ -13,6 +13,18 @@ import (
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
)
|
||||
|
||||
// shumaiMobileThreeResp 数脉 /v4/mobile_three/check 返回的 data 结构
|
||||
// result: 0-一致 1-不一致 2-无记录;channel: cmcc/cucc/ctcc/gdcc
|
||||
type shumaiMobileThreeResp struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
Result string `json:"result"`
|
||||
Desc string `json:"desc"`
|
||||
Channel string `json:"channel"`
|
||||
Sex string `json:"sex"`
|
||||
Birthday string `json:"birthday"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
// yysy9a1bOut 最终返回格式
|
||||
// result: 01一致 02不一致 03不确定 04失败/虚拟号;type: 1移动 2联通 3电信 4广电
|
||||
type yysy9a1bOut struct {
|
||||
@@ -43,7 +55,7 @@ func ProcessYYSY9A1BRequest(ctx context.Context, params []byte, deps *processors
|
||||
|
||||
// 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded
|
||||
apiPath := "/v4/mobile_three/check"
|
||||
|
||||
|
||||
// 先尝试使用政务接口(app_id2 和 app_secret2)
|
||||
respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData, true)
|
||||
if err != nil {
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
@@ -27,42 +29,27 @@ func ProcessYYSYBE08Request(ctx context.Context, params []byte, deps *processors
|
||||
|
||||
// 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded
|
||||
apiPath := "/v4/id_card/check"
|
||||
|
||||
|
||||
// 先尝试使用政务接口(app_id2 和 app_secret2)
|
||||
respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData, true)
|
||||
if err != nil {
|
||||
// 使用实时接口(app_id 和 app_secret)重试
|
||||
respBytes, err = deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData, false)
|
||||
// 如果重试后仍然失败,处理错误响应 - 转换为目标格式
|
||||
// 重试仍失败:阿里云身份证二要素兜底,并直接返回统一映射响应
|
||||
if err != nil {
|
||||
errorMsg := err.Error()
|
||||
if errorMsg == "" {
|
||||
errorMsg = "请求失败"
|
||||
}
|
||||
errorResponse := map[string]interface{}{
|
||||
"ctidRequest": map[string]interface{}{
|
||||
"ctidAuth": map[string]interface{}{
|
||||
"idCard": paramsDto.IDCard,
|
||||
"name": paramsDto.Name,
|
||||
"resultCode": "5XXX",
|
||||
"resultMsg": errorMsg,
|
||||
"verifyResult": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
return json.Marshal(errorResponse)
|
||||
return callAliyunIDCardCheckRaw(ctx, deps, paramsDto.Name, paramsDto.IDCard)
|
||||
}
|
||||
}
|
||||
|
||||
// 解析数脉 /v4/id_card/check 的 data 内容(CallAPIForm 返回的即 data 对象)
|
||||
// 数卖响应: result 0-一致 1-不一致 2-无记录(预留); desc 如 "一致"/"不一致"
|
||||
var shumaiData struct {
|
||||
Result int `json:"result"`
|
||||
OrderNo string `json:"order_no"`
|
||||
Desc string `json:"desc"`
|
||||
Sex string `json:"sex"`
|
||||
Birthday string `json:"birthday"`
|
||||
Address string `json:"address"`
|
||||
Result interface{} `json:"result"`
|
||||
OrderNo string `json:"order_no"`
|
||||
Desc string `json:"desc"`
|
||||
Sex string `json:"sex"`
|
||||
Birthday string `json:"birthday"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(respBytes, &shumaiData); err != nil {
|
||||
@@ -83,30 +70,7 @@ func ProcessYYSYBE08Request(ctx context.Context, params []byte, deps *processors
|
||||
|
||||
// 按数卖 result 验证结果处理: 0-一致 1-不一致 2-无记录(预留)
|
||||
// resultCode: 0XXX=一致, 5XXX=不一致/无记录
|
||||
var resultCode, verifyResult, resultMsg string
|
||||
switch shumaiData.Result {
|
||||
case 0: // 一致(收费)
|
||||
resultCode = "0XXX"
|
||||
verifyResult = "一致"
|
||||
resultMsg = shumaiData.Desc
|
||||
if resultMsg == "" {
|
||||
resultMsg = "成功"
|
||||
}
|
||||
case 1: // 不一致(收费)
|
||||
resultCode = "5XXX"
|
||||
verifyResult = "不一致"
|
||||
resultMsg = shumaiData.Desc
|
||||
if resultMsg == "" {
|
||||
resultMsg = "不一致"
|
||||
}
|
||||
default:
|
||||
resultCode = "5XXX"
|
||||
verifyResult = "不一致"
|
||||
resultMsg = shumaiData.Desc
|
||||
if resultMsg == "" {
|
||||
resultMsg = "不一致"
|
||||
}
|
||||
}
|
||||
resultCode, verifyResult, resultMsg := mapIDCardCheckResult(shumaiData.Result, shumaiData.Desc)
|
||||
|
||||
// 构建目标格式的响应
|
||||
response := map[string]interface{}{
|
||||
@@ -123,3 +87,48 @@ func ProcessYYSYBE08Request(ctx context.Context, params []byte, deps *processors
|
||||
|
||||
return json.Marshal(response)
|
||||
}
|
||||
|
||||
func mapIDCardCheckResult(rawResult interface{}, desc string) (resultCode, verifyResult, resultMsg string) {
|
||||
if isResultZero(rawResult) {
|
||||
resultCode = "0XXX"
|
||||
verifyResult = "一致"
|
||||
resultMsg = desc
|
||||
if resultMsg == "" {
|
||||
resultMsg = "成功"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resultCode = "5XXX"
|
||||
verifyResult = "不一致"
|
||||
resultMsg = desc
|
||||
if resultMsg == "" {
|
||||
resultMsg = "不一致"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func isResultZero(v interface{}) bool {
|
||||
switch r := v.(type) {
|
||||
case float64:
|
||||
return r == 0
|
||||
case int:
|
||||
return r == 0
|
||||
case int32:
|
||||
return r == 0
|
||||
case int64:
|
||||
return r == 0
|
||||
case json.Number:
|
||||
n, err := r.Int64()
|
||||
return err == nil && n == 0
|
||||
case string:
|
||||
s := strings.TrimSpace(r)
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
n, err := strconv.ParseFloat(s, 64)
|
||||
return err == nil && n == 0
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
package yysy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestYYSYBE08ResponseStructure(t *testing.T) {
|
||||
// 测试响应结构构建逻辑
|
||||
resultCode := "1001"
|
||||
resultMsg := "验证通过"
|
||||
verifyResult := "一致"
|
||||
|
||||
// 模拟阿里云返回result=0(一致)的情况
|
||||
alicloudResult := 0
|
||||
if alicloudResult == 0 {
|
||||
// 验证成功
|
||||
resultCode = "1001"
|
||||
resultMsg = "验证通过"
|
||||
verifyResult = "一致"
|
||||
} else {
|
||||
// 验证失败
|
||||
resultCode = "1002"
|
||||
resultMsg = "身份证号不匹配"
|
||||
verifyResult = "不一致"
|
||||
}
|
||||
|
||||
// 构建响应结构
|
||||
response := map[string]interface{}{
|
||||
"ctidRequest": map[string]interface{}{
|
||||
"ctidAuth": map[string]interface{}{
|
||||
"resultCode": resultCode,
|
||||
"resultMsg": resultMsg,
|
||||
"name": "张荣宏",
|
||||
"idCard": "45212220000827423X",
|
||||
"verifyResult": verifyResult,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 序列化为JSON
|
||||
jsonData, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
t.Fatalf("JSON序列化失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证JSON结构
|
||||
var parsedResponse map[string]interface{}
|
||||
if err := json.Unmarshal(jsonData, &parsedResponse); err != nil {
|
||||
t.Fatalf("JSON反序列化失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证字段存在
|
||||
ctidRequest, exists := parsedResponse["ctidRequest"]
|
||||
if !exists {
|
||||
t.Fatal("响应中缺少ctidRequest字段")
|
||||
}
|
||||
|
||||
ctidAuth, exists := ctidRequest.(map[string]interface{})["ctidAuth"]
|
||||
if !exists {
|
||||
t.Fatal("响应中缺少ctidAuth字段")
|
||||
}
|
||||
|
||||
authData := ctidAuth.(map[string]interface{})
|
||||
|
||||
// 验证字段值
|
||||
expectedFields := map[string]string{
|
||||
"resultCode": "1001",
|
||||
"resultMsg": "验证通过",
|
||||
"name": "张荣宏",
|
||||
"idCard": "45212220000827423X",
|
||||
"verifyResult": "一致",
|
||||
}
|
||||
|
||||
for field, expectedValue := range expectedFields {
|
||||
if authData[field] != expectedValue {
|
||||
t.Errorf("字段%s期望值为%s,实际为%s", field, expectedValue, authData[field])
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("测试成功,响应结构: %s", string(jsonData))
|
||||
}
|
||||
|
||||
func TestYYSYBE08ResponseStructure_Failure(t *testing.T) {
|
||||
// 测试验证失败的情况
|
||||
resultCode := "1002"
|
||||
resultMsg := "身份证号不匹配"
|
||||
verifyResult := "不一致"
|
||||
|
||||
// 模拟阿里云返回result=1(不一致)的情况
|
||||
alicloudResult := 1
|
||||
if alicloudResult == 0 {
|
||||
// 验证成功
|
||||
resultCode = "1001"
|
||||
resultMsg = "验证通过"
|
||||
verifyResult = "一致"
|
||||
} else {
|
||||
// 验证失败
|
||||
resultCode = "1002"
|
||||
resultMsg = "身份证号不匹配"
|
||||
verifyResult = "不一致"
|
||||
}
|
||||
|
||||
// 构建响应结构
|
||||
response := map[string]interface{}{
|
||||
"ctidRequest": map[string]interface{}{
|
||||
"ctidAuth": map[string]interface{}{
|
||||
"resultCode": resultCode,
|
||||
"resultMsg": resultMsg,
|
||||
"name": "张三",
|
||||
"idCard": "110101199001011235",
|
||||
"verifyResult": verifyResult,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 序列化为JSON
|
||||
jsonData, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
t.Fatalf("JSON序列化失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证JSON结构
|
||||
var parsedResponse map[string]interface{}
|
||||
if err := json.Unmarshal(jsonData, &parsedResponse); err != nil {
|
||||
t.Fatalf("JSON反序列化失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证字段存在
|
||||
ctidRequest, exists := parsedResponse["ctidRequest"]
|
||||
if !exists {
|
||||
t.Fatal("响应中缺少ctidRequest字段")
|
||||
}
|
||||
|
||||
ctidAuth, exists := ctidRequest.(map[string]interface{})["ctidAuth"]
|
||||
if !exists {
|
||||
t.Fatal("响应中缺少ctidAuth字段")
|
||||
}
|
||||
|
||||
authData := ctidAuth.(map[string]interface{})
|
||||
|
||||
// 验证字段值
|
||||
expectedFields := map[string]string{
|
||||
"resultCode": "1002",
|
||||
"resultMsg": "身份证号不匹配",
|
||||
"name": "张三",
|
||||
"idCard": "110101199001011235",
|
||||
"verifyResult": "不一致",
|
||||
}
|
||||
|
||||
for field, expectedValue := range expectedFields {
|
||||
if authData[field] != expectedValue {
|
||||
t.Errorf("字段%s期望值为%s,实际为%s", field, expectedValue, authData[field])
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("测试成功,失败响应结构: %s", string(jsonData))
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package yysy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/alicloud"
|
||||
)
|
||||
|
||||
// ProcessYYSYBE08testRequest 与 YYSYBE08 相同入参,底层使用阿里云市场身份证二要素校验;响应映射为 ctidRequest.ctidAuth 格式
|
||||
func ProcessYYSYBE08testRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.YYSYBE08Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
return callAliyunIDCardCheckRaw(ctx, deps, paramsDto.Name, paramsDto.IDCard)
|
||||
}
|
||||
|
||||
// callAliyunIDCardCheckRaw POST api-mall/api/id_card/check(form: name、idcard),并映射为 ctidRequest.ctidAuth 响应
|
||||
func callAliyunIDCardCheckRaw(ctx context.Context, deps *processors.ProcessorDependencies, name, idCard string) ([]byte, error) {
|
||||
_ = ctx
|
||||
reqData := map[string]interface{}{
|
||||
"name": name,
|
||||
"idcard": idCard,
|
||||
}
|
||||
respBytes, err := deps.AlicloudService.CallAPI("api-mall/api/id_card/check", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, alicloud.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
var aliyunData struct {
|
||||
Msg string `json:"msg"`
|
||||
Success bool `json:"success"`
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
Birthday string `json:"birthday"`
|
||||
Result interface{} `json:"result"`
|
||||
Address string `json:"address"`
|
||||
OrderNo string `json:"orderNo"`
|
||||
Sex string `json:"sex"`
|
||||
Desc string `json:"desc"`
|
||||
} `json:"data"`
|
||||
Result interface{} `json:"result"`
|
||||
Desc string `json:"desc"`
|
||||
}
|
||||
if err := json.Unmarshal(respBytes, &aliyunData); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
rawResult := aliyunData.Result
|
||||
rawDesc := aliyunData.Desc
|
||||
// 优先使用 code=200 时 data 内的字段;兼容旧格式直接返回 result/desc
|
||||
if aliyunData.Code == 200 {
|
||||
rawResult = aliyunData.Data.Result
|
||||
rawDesc = aliyunData.Data.Desc
|
||||
}
|
||||
|
||||
resultCode, verifyResult, resultMsg := mapIDCardCheckResult(rawResult, rawDesc)
|
||||
response := map[string]interface{}{
|
||||
"ctidRequest": map[string]interface{}{
|
||||
"ctidAuth": map[string]interface{}{
|
||||
"idCard": idCard,
|
||||
"name": name,
|
||||
"resultCode": resultCode,
|
||||
"resultMsg": resultMsg,
|
||||
"verifyResult": verifyResult,
|
||||
},
|
||||
},
|
||||
}
|
||||
return json.Marshal(response)
|
||||
}
|
||||
@@ -21,7 +21,6 @@ func ProcessYYSYK9R4Request(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建数据宝入参:姓名、身份证、手机号、银行卡号(sign 外的业务参数可按需 AES 加密后作为 bodyData)
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "c115708d915451da8f34a23e144dda6b",
|
||||
"name": paramsDto.Name,
|
||||
|
||||
@@ -153,7 +153,31 @@ func (c *Certification) TransitionTo(targetStatus enums.CertificationStatus, act
|
||||
|
||||
// ================ 业务操作方法 ================
|
||||
|
||||
// SubmitEnterpriseInfo 提交企业信息
|
||||
// SubmitEnterpriseInfoForReview 提交企业信息进入人工审核(不调用 e签宝,不生成认证链接)
|
||||
func (c *Certification) SubmitEnterpriseInfoForReview(enterpriseInfo *value_objects.EnterpriseInfo) error {
|
||||
// 已处于待审核:幂等,直接成功
|
||||
if c.Status == enums.StatusInfoPendingReview {
|
||||
return nil
|
||||
}
|
||||
if c.Status != enums.StatusPending && c.Status != enums.StatusInfoRejected {
|
||||
return fmt.Errorf("当前状态 %s 不允许提交企业信息", enums.GetStatusName(c.Status))
|
||||
}
|
||||
if err := enterpriseInfo.Validate(); err != nil {
|
||||
return fmt.Errorf("企业信息验证失败: %w", err)
|
||||
}
|
||||
if err := c.TransitionTo(enums.StatusInfoPendingReview, enums.ActorTypeUser, c.UserID, "用户提交企业信息,等待人工审核"); err != nil {
|
||||
return err
|
||||
}
|
||||
c.addDomainEvent(&EnterpriseInfoSubmittedEvent{
|
||||
CertificationID: c.ID,
|
||||
UserID: c.UserID,
|
||||
EnterpriseInfo: enterpriseInfo,
|
||||
SubmittedAt: time.Now(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// SubmitEnterpriseInfo 提交企业信息(直接进入已提交,含认证链接;用于无审核或管理员审核通过后补链)
|
||||
func (c *Certification) SubmitEnterpriseInfo(enterpriseInfo *value_objects.EnterpriseInfo, authURL string, authFlowID string) error {
|
||||
// 验证当前状态
|
||||
if c.Status != enums.StatusPending && c.Status != enums.StatusInfoRejected {
|
||||
@@ -186,6 +210,33 @@ func (c *Certification) SubmitEnterpriseInfo(enterpriseInfo *value_objects.Enter
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApproveEnterpriseInfoReview 管理员审核通过:从待审核转为已提交,并写入企业认证链接
|
||||
func (c *Certification) ApproveEnterpriseInfoReview(authURL, authFlowID string, actorID string) error {
|
||||
if c.Status != enums.StatusInfoPendingReview {
|
||||
return fmt.Errorf("当前状态 %s 不允许执行审核通过", enums.GetStatusName(c.Status))
|
||||
}
|
||||
c.AuthURL = authURL
|
||||
c.AuthFlowID = authFlowID
|
||||
if err := c.TransitionTo(enums.StatusInfoSubmitted, enums.ActorTypeAdmin, actorID, "管理员审核通过"); err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
c.InfoSubmittedAt = &now
|
||||
return nil
|
||||
}
|
||||
|
||||
// RejectEnterpriseInfoReview 管理员审核拒绝
|
||||
func (c *Certification) RejectEnterpriseInfoReview(actorID, message string) error {
|
||||
if c.Status != enums.StatusInfoPendingReview {
|
||||
return fmt.Errorf("当前状态 %s 不允许执行审核拒绝", enums.GetStatusName(c.Status))
|
||||
}
|
||||
c.setFailureInfo(enums.FailureReasonManualReviewRejected, message)
|
||||
if err := c.TransitionTo(enums.StatusInfoRejected, enums.ActorTypeAdmin, actorID, "管理员审核拒绝"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 完成企业认证
|
||||
func (c *Certification) CompleteEnterpriseVerification() error {
|
||||
if c.Status != enums.StatusInfoSubmitted {
|
||||
@@ -448,6 +499,8 @@ func (c *Certification) CompleteCertification() error {
|
||||
func (c *Certification) GetDataByStatus() map[string]interface{} {
|
||||
data := map[string]interface{}{}
|
||||
switch c.Status {
|
||||
case enums.StatusInfoPendingReview:
|
||||
// 待审核,无额外数据
|
||||
case enums.StatusInfoSubmitted:
|
||||
data["auth_url"] = c.AuthURL
|
||||
case enums.StatusInfoRejected:
|
||||
@@ -494,6 +547,8 @@ func (c *Certification) GetAvailableActions() []string {
|
||||
switch c.Status {
|
||||
case enums.StatusPending:
|
||||
actions = append(actions, "submit_enterprise_info")
|
||||
case enums.StatusInfoPendingReview:
|
||||
// 等待人工审核,无用户操作
|
||||
case enums.StatusEnterpriseVerified:
|
||||
actions = append(actions, "apply_contract")
|
||||
case enums.StatusInfoRejected, enums.StatusContractRejected, enums.StatusContractExpired:
|
||||
@@ -587,8 +642,9 @@ func (c *Certification) ValidateBusinessRules() error {
|
||||
|
||||
// validateActorPermission 验证操作者权限
|
||||
func (c *Certification) validateActorPermission(targetStatus enums.CertificationStatus, actor enums.ActorType) bool {
|
||||
// 定义状态转换的权限规则
|
||||
// 定义状态转换的权限规则(目标状态 -> 允许的操作者)
|
||||
permissions := map[enums.CertificationStatus][]enums.ActorType{
|
||||
enums.StatusInfoPendingReview: {enums.ActorTypeUser},
|
||||
enums.StatusInfoSubmitted: {enums.ActorTypeUser, enums.ActorTypeAdmin},
|
||||
enums.StatusEnterpriseVerified: {enums.ActorTypeEsign, enums.ActorTypeSystem, enums.ActorTypeAdmin},
|
||||
enums.StatusInfoRejected: {enums.ActorTypeEsign, enums.ActorTypeSystem, enums.ActorTypeAdmin},
|
||||
|
||||
@@ -19,6 +19,21 @@ type EnterpriseInfoSubmitRecord struct {
|
||||
LegalPersonID string `json:"legal_person_id" gorm:"type:varchar(50);not null"`
|
||||
LegalPersonPhone string `json:"legal_person_phone" gorm:"type:varchar(50);not null"`
|
||||
EnterpriseAddress string `json:"enterprise_address" gorm:"type:varchar(200);not null"` // 新增企业地址
|
||||
|
||||
// 授权代表信息(gorm 指定列名,确保与表 enterprise_info_submit_records 列一致并正确读入)
|
||||
AuthorizedRepName string `json:"authorized_rep_name" gorm:"column:authorized_rep_name;type:varchar(50);comment:授权代表姓名"`
|
||||
AuthorizedRepID string `json:"authorized_rep_id" gorm:"column:authorized_rep_id;type:varchar(50);comment:授权代表身份证号"`
|
||||
AuthorizedRepPhone string `json:"authorized_rep_phone" gorm:"column:authorized_rep_phone;type:varchar(50);comment:授权代表手机号"`
|
||||
// 授权代表身份证正反面图片URL列表(JSON字符串),按顺序存储[人像面, 国徽面]
|
||||
AuthorizedRepIDImageURLs string `json:"authorized_rep_id_image_urls" gorm:"column:authorized_rep_id_image_urls;type:text;comment:授权代表身份证正反面图片URL列表(JSON字符串)"`
|
||||
|
||||
// 企业资质与场地材料
|
||||
BusinessLicenseImageURL string `json:"business_license_image_url" gorm:"type:varchar(500);comment:营业执照图片URL"`
|
||||
OfficePlaceImageURLs string `json:"office_place_image_urls" gorm:"type:text;comment:办公场地图片URL列表(JSON字符串)"`
|
||||
// 应用场景
|
||||
APIUsage string `json:"api_usage" gorm:"type:text;comment:接口用途及业务场景说明"`
|
||||
ScenarioAttachmentURLs string `json:"scenario_attachment_urls" gorm:"type:text;comment:场景附件图片URL列表(JSON字符串)"`
|
||||
|
||||
// 提交状态
|
||||
Status string `json:"status" gorm:"type:varchar(20);not null;default:'submitted'"` // submitted, verified, failed
|
||||
SubmitAt time.Time `json:"submit_at" gorm:"not null"`
|
||||
@@ -26,6 +41,12 @@ type EnterpriseInfoSubmitRecord struct {
|
||||
FailedAt *time.Time `json:"failed_at"`
|
||||
FailureReason string `json:"failure_reason" gorm:"type:text"`
|
||||
|
||||
// 人工审核信息
|
||||
ManualReviewStatus string `json:"manual_review_status" gorm:"type:varchar(20);not null;default:'pending';comment:人工审核状态(pending,approved,rejected)"`
|
||||
ManualReviewRemark string `json:"manual_review_remark" gorm:"type:text;comment:人工审核备注"`
|
||||
ManualReviewedAt *time.Time `json:"manual_reviewed_at" gorm:"comment:人工审核时间"`
|
||||
ManualReviewerID string `json:"manual_reviewer_id" gorm:"type:varchar(36);comment:人工审核人ID"`
|
||||
|
||||
// 系统字段
|
||||
CreatedAt time.Time `json:"created_at" gorm:"not null"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"not null"`
|
||||
@@ -42,18 +63,19 @@ func NewEnterpriseInfoSubmitRecord(
|
||||
userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress string,
|
||||
) *EnterpriseInfoSubmitRecord {
|
||||
return &EnterpriseInfoSubmitRecord{
|
||||
ID: uuid.New().String(),
|
||||
UserID: userID,
|
||||
CompanyName: companyName,
|
||||
UnifiedSocialCode: unifiedSocialCode,
|
||||
LegalPersonName: legalPersonName,
|
||||
LegalPersonID: legalPersonID,
|
||||
LegalPersonPhone: legalPersonPhone,
|
||||
EnterpriseAddress: enterpriseAddress,
|
||||
Status: "submitted",
|
||||
SubmitAt: time.Now(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
ID: uuid.New().String(),
|
||||
UserID: userID,
|
||||
CompanyName: companyName,
|
||||
UnifiedSocialCode: unifiedSocialCode,
|
||||
LegalPersonName: legalPersonName,
|
||||
LegalPersonID: legalPersonID,
|
||||
LegalPersonPhone: legalPersonPhone,
|
||||
EnterpriseAddress: enterpriseAddress,
|
||||
Status: "submitted",
|
||||
ManualReviewStatus: "pending",
|
||||
SubmitAt: time.Now(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +96,26 @@ func (r *EnterpriseInfoSubmitRecord) MarkAsFailed(reason string) {
|
||||
r.UpdatedAt = now
|
||||
}
|
||||
|
||||
// MarkManualApproved 标记人工审核通过
|
||||
func (r *EnterpriseInfoSubmitRecord) MarkManualApproved(reviewerID, remark string) {
|
||||
now := time.Now()
|
||||
r.ManualReviewStatus = "approved"
|
||||
r.ManualReviewedAt = &now
|
||||
r.ManualReviewerID = reviewerID
|
||||
r.ManualReviewRemark = remark
|
||||
r.UpdatedAt = now
|
||||
}
|
||||
|
||||
// MarkManualRejected 标记人工审核拒绝
|
||||
func (r *EnterpriseInfoSubmitRecord) MarkManualRejected(reviewerID, remark string) {
|
||||
now := time.Now()
|
||||
r.ManualReviewStatus = "rejected"
|
||||
r.ManualReviewedAt = &now
|
||||
r.ManualReviewerID = reviewerID
|
||||
r.ManualReviewRemark = remark
|
||||
r.UpdatedAt = now
|
||||
}
|
||||
|
||||
// IsVerified 检查是否已验证
|
||||
func (r *EnterpriseInfoSubmitRecord) IsVerified() bool {
|
||||
return r.Status == "verified"
|
||||
|
||||
@@ -5,12 +5,13 @@ type CertificationStatus string
|
||||
|
||||
const (
|
||||
// === 主流程状态 ===
|
||||
StatusPending CertificationStatus = "pending" // 待认证
|
||||
StatusInfoSubmitted CertificationStatus = "info_submitted" // 已提交企业信息
|
||||
StatusEnterpriseVerified CertificationStatus = "enterprise_verified" // 已企业认证
|
||||
StatusContractApplied CertificationStatus = "contract_applied" // 已申请签署合同
|
||||
StatusContractSigned CertificationStatus = "contract_signed" // 已签署合同
|
||||
StatusCompleted CertificationStatus = "completed" // 认证完成
|
||||
StatusPending CertificationStatus = "pending" // 待认证
|
||||
StatusInfoPendingReview CertificationStatus = "info_pending_review" // 企业信息待人工审核
|
||||
StatusInfoSubmitted CertificationStatus = "info_submitted" // 已提交企业信息(审核通过)
|
||||
StatusEnterpriseVerified CertificationStatus = "enterprise_verified" // 已企业认证
|
||||
StatusContractApplied CertificationStatus = "contract_applied" // 已申请签署合同
|
||||
StatusContractSigned CertificationStatus = "contract_signed" // 已签署合同
|
||||
StatusCompleted CertificationStatus = "completed" // 认证完成
|
||||
|
||||
// === 失败状态 ===
|
||||
StatusInfoRejected CertificationStatus = "info_rejected" // 企业信息被拒绝
|
||||
@@ -21,6 +22,7 @@ const (
|
||||
// AllStatuses 所有有效状态列表
|
||||
var AllStatuses = []CertificationStatus{
|
||||
StatusPending,
|
||||
StatusInfoPendingReview,
|
||||
StatusInfoSubmitted,
|
||||
StatusEnterpriseVerified,
|
||||
StatusContractApplied,
|
||||
@@ -34,6 +36,7 @@ var AllStatuses = []CertificationStatus{
|
||||
// MainFlowStatuses 主流程状态列表
|
||||
var MainFlowStatuses = []CertificationStatus{
|
||||
StatusPending,
|
||||
StatusInfoPendingReview,
|
||||
StatusInfoSubmitted,
|
||||
StatusEnterpriseVerified,
|
||||
StatusContractApplied,
|
||||
@@ -61,6 +64,7 @@ func IsValidStatus(status CertificationStatus) bool {
|
||||
func GetStatusName(status CertificationStatus) string {
|
||||
statusNames := map[CertificationStatus]string{
|
||||
StatusPending: "待认证",
|
||||
StatusInfoPendingReview: "企业信息待审核",
|
||||
StatusInfoSubmitted: "已提交企业信息",
|
||||
StatusEnterpriseVerified: "已企业认证",
|
||||
StatusContractApplied: "已申请签署合同",
|
||||
@@ -120,14 +124,15 @@ func GetStatusCategory(status CertificationStatus) string {
|
||||
func GetStatusPriority(status CertificationStatus) int {
|
||||
priorities := map[CertificationStatus]int{
|
||||
StatusPending: 1,
|
||||
StatusInfoSubmitted: 2,
|
||||
StatusEnterpriseVerified: 3,
|
||||
StatusContractApplied: 4,
|
||||
StatusContractSigned: 5,
|
||||
StatusCompleted: 6,
|
||||
StatusInfoRejected: 7,
|
||||
StatusContractRejected: 8,
|
||||
StatusContractExpired: 9,
|
||||
StatusInfoPendingReview: 2,
|
||||
StatusInfoSubmitted: 3,
|
||||
StatusEnterpriseVerified: 4,
|
||||
StatusContractApplied: 5,
|
||||
StatusContractSigned: 6,
|
||||
StatusCompleted: 7,
|
||||
StatusInfoRejected: 8,
|
||||
StatusContractRejected: 9,
|
||||
StatusContractExpired: 10,
|
||||
}
|
||||
|
||||
if priority, exists := priorities[status]; exists {
|
||||
@@ -140,6 +145,7 @@ func GetStatusPriority(status CertificationStatus) int {
|
||||
func GetProgressPercentage(status CertificationStatus) int {
|
||||
progressMap := map[CertificationStatus]int{
|
||||
StatusPending: 0,
|
||||
StatusInfoPendingReview: 15,
|
||||
StatusInfoSubmitted: 25,
|
||||
StatusEnterpriseVerified: 50,
|
||||
StatusContractApplied: 75,
|
||||
@@ -160,7 +166,8 @@ func GetProgressPercentage(status CertificationStatus) int {
|
||||
func IsUserActionRequired(status CertificationStatus) bool {
|
||||
userActionRequired := map[CertificationStatus]bool{
|
||||
StatusPending: true, // 需要提交企业信息
|
||||
StatusInfoSubmitted: false, // 等待系统验证
|
||||
StatusInfoPendingReview: false, // 等待人工审核
|
||||
StatusInfoSubmitted: false, // 等待完成企业认证
|
||||
StatusEnterpriseVerified: true, // 需要申请合同
|
||||
StatusContractApplied: true, // 需要签署合同
|
||||
StatusContractSigned: false, // 合同已签署,等待系统处理
|
||||
@@ -180,6 +187,7 @@ func IsUserActionRequired(status CertificationStatus) bool {
|
||||
func GetUserActionHint(status CertificationStatus) string {
|
||||
hints := map[CertificationStatus]string{
|
||||
StatusPending: "请提交企业信息",
|
||||
StatusInfoPendingReview: "企业信息已提交,请等待管理员审核",
|
||||
StatusInfoSubmitted: "请完成企业认证",
|
||||
StatusEnterpriseVerified: "企业认证完成,请申请签署合同",
|
||||
StatusContractApplied: "请在规定时间内完成合同签署",
|
||||
@@ -200,8 +208,13 @@ func GetUserActionHint(status CertificationStatus) string {
|
||||
func GetNextValidStatuses(currentStatus CertificationStatus) []CertificationStatus {
|
||||
nextStatusMap := map[CertificationStatus][]CertificationStatus{
|
||||
StatusPending: {
|
||||
StatusInfoPendingReview, // 用户提交企业信息,进入待审核
|
||||
StatusInfoSubmitted, // 暂时跳过人工审核,直接进入已提交
|
||||
StatusCompleted,
|
||||
},
|
||||
StatusInfoPendingReview: {
|
||||
StatusInfoSubmitted,
|
||||
// 管理员/系统可直接完成认证
|
||||
StatusInfoRejected,
|
||||
StatusCompleted,
|
||||
},
|
||||
StatusInfoSubmitted: {
|
||||
@@ -265,15 +278,18 @@ func CanTransitionTo(currentStatus, targetStatus CertificationStatus) bool {
|
||||
// GetTransitionReason 获取状态转换的原因描述
|
||||
func GetTransitionReason(from, to CertificationStatus) string {
|
||||
transitionReasons := map[string]string{
|
||||
string(StatusPending) + "->" + string(StatusInfoSubmitted): "用户提交企业信息",
|
||||
string(StatusInfoSubmitted) + "->" + string(StatusEnterpriseVerified): "e签宝企业认证成功",
|
||||
string(StatusInfoSubmitted) + "->" + string(StatusInfoRejected): "e签宝企业认证失败",
|
||||
string(StatusEnterpriseVerified) + "->" + string(StatusContractApplied): "用户申请签署合同",
|
||||
string(StatusContractApplied) + "->" + string(StatusContractSigned): "e签宝合同签署成功",
|
||||
string(StatusContractSigned) + "->" + string(StatusCompleted): "系统处理完成,认证成功",
|
||||
string(StatusContractApplied) + "->" + string(StatusContractRejected): "用户拒绝签署合同",
|
||||
string(StatusContractApplied) + "->" + string(StatusContractExpired): "合同签署超时",
|
||||
string(StatusInfoRejected) + "->" + string(StatusInfoSubmitted): "用户重新提交企业信息",
|
||||
string(StatusPending) + "->" + string(StatusInfoPendingReview): "用户提交企业信息,等待人工审核",
|
||||
string(StatusInfoPendingReview) + "->" + string(StatusInfoSubmitted): "管理员审核通过",
|
||||
string(StatusInfoPendingReview) + "->" + string(StatusInfoRejected): "管理员审核拒绝",
|
||||
string(StatusPending) + "->" + string(StatusInfoSubmitted): "用户提交企业信息",
|
||||
string(StatusInfoSubmitted) + "->" + string(StatusEnterpriseVerified): "e签宝企业认证成功",
|
||||
string(StatusInfoSubmitted) + "->" + string(StatusInfoRejected): "e签宝企业认证失败",
|
||||
string(StatusEnterpriseVerified) + "->" + string(StatusContractApplied): "用户申请签署合同",
|
||||
string(StatusContractApplied) + "->" + string(StatusContractSigned): "e签宝合同签署成功",
|
||||
string(StatusContractSigned) + "->" + string(StatusCompleted): "系统处理完成,认证成功",
|
||||
string(StatusContractApplied) + "->" + string(StatusContractRejected): "用户拒绝签署合同",
|
||||
string(StatusContractApplied) + "->" + string(StatusContractExpired): "合同签署超时",
|
||||
string(StatusInfoRejected) + "->" + string(StatusInfoSubmitted): "用户重新提交企业信息",
|
||||
string(StatusContractRejected) + "->" + string(StatusEnterpriseVerified): "重置状态,准备重新申请",
|
||||
string(StatusContractExpired) + "->" + string(StatusEnterpriseVerified): "重置状态,准备重新申请",
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ const (
|
||||
FailureReasonLegalPersonMismatch FailureReason = "legal_person_mismatch" // 法定代表人信息不匹配
|
||||
FailureReasonEsignVerificationFailed FailureReason = "esign_verification_failed" // e签宝验证失败
|
||||
FailureReasonInvalidDocument FailureReason = "invalid_document" // 证件信息无效
|
||||
FailureReasonManualReviewRejected FailureReason = "manual_review_rejected" // 人工审核拒绝
|
||||
|
||||
// === 合同签署失败原因 ===
|
||||
FailureReasonContractRejectedByUser FailureReason = "contract_rejected_by_user" // 用户拒绝签署
|
||||
@@ -35,7 +36,7 @@ var AllFailureReasons = []FailureReason{
|
||||
FailureReasonLegalPersonMismatch,
|
||||
FailureReasonEsignVerificationFailed,
|
||||
FailureReasonInvalidDocument,
|
||||
|
||||
FailureReasonManualReviewRejected,
|
||||
// 合同签署失败
|
||||
FailureReasonContractRejectedByUser,
|
||||
FailureReasonContractExpired,
|
||||
@@ -97,7 +98,7 @@ func GetFailureReasonName(reason FailureReason) string {
|
||||
FailureReasonLegalPersonMismatch: "法定代表人信息不匹配",
|
||||
FailureReasonEsignVerificationFailed: "e签宝验证失败",
|
||||
FailureReasonInvalidDocument: "证件信息无效",
|
||||
|
||||
FailureReasonManualReviewRejected: "人工审核拒绝",
|
||||
// 合同签署失败
|
||||
FailureReasonContractRejectedByUser: "用户拒绝签署",
|
||||
FailureReasonContractExpired: "合同签署超时",
|
||||
@@ -128,7 +129,7 @@ func GetFailureReasonCategory(reason FailureReason) string {
|
||||
FailureReasonLegalPersonMismatch: "企业验证",
|
||||
FailureReasonEsignVerificationFailed: "企业验证",
|
||||
FailureReasonInvalidDocument: "企业验证",
|
||||
|
||||
FailureReasonManualReviewRejected: "人工审核",
|
||||
// 合同签署失败
|
||||
FailureReasonContractRejectedByUser: "合同签署",
|
||||
FailureReasonContractExpired: "合同签署",
|
||||
@@ -189,7 +190,7 @@ func GetSuggestedAction(reason FailureReason) string {
|
||||
FailureReasonLegalPersonMismatch: "请核对法定代表人信息是否正确",
|
||||
FailureReasonEsignVerificationFailed: "请稍后重试,如持续失败请联系客服",
|
||||
FailureReasonInvalidDocument: "请检查证件信息是否有效",
|
||||
|
||||
FailureReasonManualReviewRejected: "请根据审核意见修正后重新提交,或联系客服",
|
||||
// 合同签署失败
|
||||
FailureReasonContractRejectedByUser: "您可以重新申请签署合同",
|
||||
FailureReasonContractExpired: "请重新申请签署合同",
|
||||
@@ -220,7 +221,7 @@ func IsRetryable(reason FailureReason) bool {
|
||||
FailureReasonLegalPersonMismatch: true,
|
||||
FailureReasonEsignVerificationFailed: true, // 可能是临时问题
|
||||
FailureReasonInvalidDocument: true,
|
||||
|
||||
FailureReasonManualReviewRejected: true, // 用户可修正后重新提交
|
||||
// 合同签署失败
|
||||
FailureReasonContractRejectedByUser: true, // 用户可以改变主意
|
||||
FailureReasonContractExpired: true, // 可以重新申请
|
||||
@@ -253,6 +254,7 @@ func GetRetrySuggestion(reason FailureReason) string {
|
||||
FailureReasonLegalPersonMismatch: "请确认法定代表人信息后重新提交",
|
||||
FailureReasonEsignVerificationFailed: "请稍后重新尝试",
|
||||
FailureReasonInvalidDocument: "请检查证件信息后重新提交",
|
||||
FailureReasonManualReviewRejected: "请根据审核意见修正企业信息后重新提交",
|
||||
FailureReasonContractRejectedByUser: "如需要可重新申请合同",
|
||||
FailureReasonContractExpired: "请重新申请合同签署",
|
||||
FailureReasonSignProcessFailed: "请重新尝试签署",
|
||||
|
||||
@@ -5,10 +5,30 @@ import (
|
||||
"tyapi-server/internal/domains/certification/entities"
|
||||
)
|
||||
|
||||
// ListSubmitRecordsFilter 提交记录列表筛选(以状态机 certification 状态为准)
|
||||
type ListSubmitRecordsFilter struct {
|
||||
CertificationStatus string // 认证状态筛选,如 info_pending_review / info_submitted / info_rejected,空为全部
|
||||
CompanyName string // 企业名称(模糊搜索)
|
||||
LegalPersonPhone string // 法人手机号
|
||||
LegalPersonName string // 法人姓名(模糊搜索)
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
// ListSubmitRecordsResult 列表结果
|
||||
type ListSubmitRecordsResult struct {
|
||||
Records []*entities.EnterpriseInfoSubmitRecord
|
||||
Total int64
|
||||
}
|
||||
|
||||
type EnterpriseInfoSubmitRecordRepository interface {
|
||||
Create(ctx context.Context, record *entities.EnterpriseInfoSubmitRecord) error
|
||||
Update(ctx context.Context, record *entities.EnterpriseInfoSubmitRecord) error
|
||||
Exists(ctx context.Context, ID string) (bool, error)
|
||||
FindByID(ctx context.Context, id string) (*entities.EnterpriseInfoSubmitRecord, error)
|
||||
FindLatestByUserID(ctx context.Context, userID string) (*entities.EnterpriseInfoSubmitRecord, error)
|
||||
FindLatestVerifiedByUserID(ctx context.Context, userID string) (*entities.EnterpriseInfoSubmitRecord, error)
|
||||
// ExistsByUnifiedSocialCodeExcludeUser 检查该统一社会信用代码是否已被其他用户提交(已提交/已通过验证,排除指定用户)
|
||||
ExistsByUnifiedSocialCodeExcludeUser(ctx context.Context, unifiedSocialCode string, excludeUserID string) (bool, error)
|
||||
List(ctx context.Context, filter ListSubmitRecordsFilter) (*ListSubmitRecordsResult, error)
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ func (s *EnterpriseInfoSubmitRecordService) ValidateWithWestdex(ctx context.Cont
|
||||
// 调用QYGL23T7处理器进行验证
|
||||
responseBytes, err := qygl.ProcessQYGL23T7Request(ctx, paramsBytes, deps)
|
||||
if err != nil {
|
||||
// 检查是否是数据源错误
|
||||
// 检查是否是数据源错误企业信息不一致
|
||||
if errors.Is(err, processors.ErrDatasource) {
|
||||
return fmt.Errorf("数据源异常: %w", err)
|
||||
}
|
||||
|
||||
21
internal/domains/security/entities/suspicious_ip_record.go
Normal file
21
internal/domains/security/entities/suspicious_ip_record.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package entities
|
||||
|
||||
import "time"
|
||||
|
||||
// SuspiciousIPRecord 可疑IP请求记录
|
||||
type SuspiciousIPRecord struct {
|
||||
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
IP string `gorm:"type:varchar(64);not null;index:idx_ip_created,priority:1" json:"ip"`
|
||||
Path string `gorm:"type:varchar(255);not null;index:idx_path_created,priority:1" json:"path"`
|
||||
Method string `gorm:"type:varchar(16);not null;default:GET" json:"method"`
|
||||
RequestCount int `gorm:"not null;default:1" json:"request_count"`
|
||||
WindowSeconds int `gorm:"not null;default:10" json:"window_seconds"`
|
||||
TriggerReason string `gorm:"type:varchar(64);not null;default:rate_limit" json:"trigger_reason"`
|
||||
UserAgent string `gorm:"type:varchar(512);not null;default:''" json:"user_agent"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime;index:idx_ip_created,priority:2;index:idx_path_created,priority:2;index:idx_created" json:"created_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (SuspiciousIPRecord) TableName() string {
|
||||
return "suspicious_ip_records"
|
||||
}
|
||||
46
internal/domains/subordinate/entities/invitation.go
Normal file
46
internal/domains/subordinate/entities/invitation.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// InvitationStatus 邀请状态
|
||||
type InvitationStatus string
|
||||
|
||||
const (
|
||||
InvitationStatusPending InvitationStatus = "pending"
|
||||
InvitationStatusConsumed InvitationStatus = "consumed"
|
||||
InvitationStatusRevoked InvitationStatus = "revoked"
|
||||
)
|
||||
|
||||
// SubordinateInvitation 主账号邀请记录(存 token 哈希)
|
||||
type SubordinateInvitation struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"唯一标识"`
|
||||
ParentUserID string `gorm:"type:varchar(36);not null;index" json:"parent_user_id" comment:"主账号用户ID"`
|
||||
TokenHash string `gorm:"type:varchar(64);not null;uniqueIndex" json:"-" comment:"邀请码的SHA256(十六进制)"`
|
||||
ExpiresAt time.Time `gorm:"not null;index" json:"expires_at" comment:"过期时间"`
|
||||
Status InvitationStatus `gorm:"type:varchar(20);not null;default:pending" json:"status" comment:"状态"`
|
||||
|
||||
ConsumedByUserID *string `gorm:"type:varchar(36);index" json:"consumed_by_user_id,omitempty" comment:"核销后的子账号用户ID"`
|
||||
ConsumedAt *time.Time `json:"consumed_at,omitempty" comment:"核销时间"`
|
||||
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 表名
|
||||
func (SubordinateInvitation) TableName() string {
|
||||
return "subordinate_invitations"
|
||||
}
|
||||
|
||||
// BeforeCreate 生成ID
|
||||
func (i *SubordinateInvitation) BeforeCreate(tx *gorm.DB) error {
|
||||
if i.ID == "" {
|
||||
i.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
42
internal/domains/subordinate/entities/link.go
Normal file
42
internal/domains/subordinate/entities/link.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// LinkStatus 主从关系状态
|
||||
type LinkStatus string
|
||||
|
||||
const (
|
||||
LinkStatusActive LinkStatus = "active"
|
||||
LinkStatusRevoked LinkStatus = "revoked"
|
||||
)
|
||||
|
||||
// UserSubordinateLink 主账号与下属关系
|
||||
type UserSubordinateLink struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"唯一标识"`
|
||||
ParentUserID string `gorm:"type:varchar(36);not null;index:idx_parent,priority:1" json:"parent_user_id" comment:"主账号用户ID"`
|
||||
ChildUserID string `gorm:"type:varchar(36);not null;uniqueIndex" json:"child_user_id" comment:"子账号用户ID(唯一)"`
|
||||
InvitationID *string `gorm:"type:varchar(36);index" json:"invitation_id,omitempty" comment:"关联的邀请ID"`
|
||||
Status LinkStatus `gorm:"type:varchar(20);not null;default:active" json:"status" comment:"状态"`
|
||||
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 表名
|
||||
func (UserSubordinateLink) TableName() string {
|
||||
return "user_subordinate_links"
|
||||
}
|
||||
|
||||
// BeforeCreate 生成ID
|
||||
func (l *UserSubordinateLink) BeforeCreate(tx *gorm.DB) error {
|
||||
if l.ID == "" {
|
||||
l.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
98
internal/domains/subordinate/entities/quota.go
Normal file
98
internal/domains/subordinate/entities/quota.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
// QuotaLedgerChangeTypePurchaseForSub 主账号为子账号购买额度
|
||||
QuotaLedgerChangeTypePurchaseForSub = "purchase_for_sub"
|
||||
// QuotaLedgerChangeTypeConsumeAPI 用户调用API消耗额度
|
||||
QuotaLedgerChangeTypeConsumeAPI = "api_consume"
|
||||
)
|
||||
|
||||
// SubordinateQuotaPurchase 主账号为子账号购买额度记录
|
||||
type SubordinateQuotaPurchase struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
|
||||
ParentUserID string `gorm:"type:varchar(36);not null;index" json:"parent_user_id"`
|
||||
ChildUserID string `gorm:"type:varchar(36);not null;index" json:"child_user_id"`
|
||||
ProductID string `gorm:"type:varchar(36);not null;index" json:"product_id"`
|
||||
CallCount int64 `gorm:"type:bigint;not null" json:"call_count"`
|
||||
UnitPrice decimal.Decimal `gorm:"type:decimal(20,8);not null" json:"unit_price"`
|
||||
TotalAmount decimal.Decimal `gorm:"type:decimal(20,8);not null" json:"total_amount"`
|
||||
BusinessRef string `gorm:"type:varchar(64);not null;uniqueIndex" json:"business_ref"`
|
||||
OperatorUserID string `gorm:"type:varchar(36);not null" json:"operator_user_id"`
|
||||
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
func (SubordinateQuotaPurchase) TableName() string {
|
||||
return "subordinate_quota_purchases"
|
||||
}
|
||||
|
||||
func (q *SubordinateQuotaPurchase) BeforeCreate(tx *gorm.DB) error {
|
||||
if q.ID == "" {
|
||||
q.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserProductQuotaAccount 用户产品额度账户(通用模型,适配所有用户)
|
||||
type UserProductQuotaAccount struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
|
||||
UserID string `gorm:"type:varchar(36);not null;index:idx_user_product,unique" json:"user_id"`
|
||||
ProductID string `gorm:"type:varchar(36);not null;index:idx_user_product,unique" json:"product_id"`
|
||||
TotalQuota int64 `gorm:"type:bigint;not null;default:0" json:"total_quota"`
|
||||
UsedQuota int64 `gorm:"type:bigint;not null;default:0" json:"used_quota"`
|
||||
AvailableQuota int64 `gorm:"type:bigint;not null;default:0" json:"available_quota"`
|
||||
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
func (UserProductQuotaAccount) TableName() string {
|
||||
return "user_product_quota_accounts"
|
||||
}
|
||||
|
||||
func (a *UserProductQuotaAccount) BeforeCreate(tx *gorm.DB) error {
|
||||
if a.ID == "" {
|
||||
a.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserProductQuotaLedger 用户产品额度流水(通用模型,适配所有用户)
|
||||
type UserProductQuotaLedger struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
|
||||
UserID string `gorm:"type:varchar(36);not null;index" json:"user_id"`
|
||||
ProductID string `gorm:"type:varchar(36);not null;index" json:"product_id"`
|
||||
ChangeType string `gorm:"type:varchar(50);not null;index" json:"change_type"`
|
||||
DeltaQuota int64 `gorm:"type:bigint;not null" json:"delta_quota"`
|
||||
BeforeQuota int64 `gorm:"type:bigint;not null" json:"before_quota"`
|
||||
AfterQuota int64 `gorm:"type:bigint;not null" json:"after_quota"`
|
||||
SourceID string `gorm:"type:varchar(36);index" json:"source_id"`
|
||||
OperatorID string `gorm:"type:varchar(36);not null" json:"operator_id"`
|
||||
Remark string `gorm:"type:varchar(255)" json:"remark"`
|
||||
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
func (UserProductQuotaLedger) TableName() string {
|
||||
return "user_product_quota_ledgers"
|
||||
}
|
||||
|
||||
func (l *UserProductQuotaLedger) BeforeCreate(tx *gorm.DB) error {
|
||||
if l.ID == "" {
|
||||
l.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
36
internal/domains/subordinate/entities/wallet_allocation.go
Normal file
36
internal/domains/subordinate/entities/wallet_allocation.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SubordinateWalletAllocation 主账号向下属余额划拨记录
|
||||
type SubordinateWalletAllocation struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"唯一标识"`
|
||||
FromUserID string `gorm:"type:varchar(36);not null;index" json:"from_user_id" comment:"主账号用户ID"`
|
||||
ToUserID string `gorm:"type:varchar(36);not null;index" json:"to_user_id" comment:"子账号用户ID"`
|
||||
Amount decimal.Decimal `gorm:"type:decimal(20,8);not null" json:"amount" comment:"金额"`
|
||||
BusinessRef string `gorm:"type:varchar(64);not null;index" json:"business_ref" comment:"业务单号(幂等/对账)"`
|
||||
OperatorUserID string `gorm:"type:varchar(36);not null" json:"operator_user_id" comment:"操作者(一般同主账号)"`
|
||||
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 表名
|
||||
func (SubordinateWalletAllocation) TableName() string {
|
||||
return "subordinate_wallet_allocations"
|
||||
}
|
||||
|
||||
// BeforeCreate 生成ID
|
||||
func (a *SubordinateWalletAllocation) BeforeCreate(tx *gorm.DB) error {
|
||||
if a.ID == "" {
|
||||
a.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"tyapi-server/internal/domains/subordinate/entities"
|
||||
)
|
||||
|
||||
// SubordinateRepository 下属模块仓储
|
||||
type SubordinateRepository interface {
|
||||
// 邀请
|
||||
CreateInvitation(ctx context.Context, inv *entities.SubordinateInvitation) error
|
||||
FindInvitationByTokenHash(ctx context.Context, tokenHash string) (*entities.SubordinateInvitation, error)
|
||||
FindInvitationByID(ctx context.Context, id string) (*entities.SubordinateInvitation, error)
|
||||
UpdateInvitation(ctx context.Context, inv *entities.SubordinateInvitation) error
|
||||
ConsumeInvitation(ctx context.Context, invitationID, childUserID string, consumedAt time.Time) (bool, error)
|
||||
ListInvitationsByParent(ctx context.Context, parentUserID string, limit, offset int) ([]*entities.SubordinateInvitation, int64, error)
|
||||
|
||||
// 主从
|
||||
CreateLink(ctx context.Context, link *entities.UserSubordinateLink) error
|
||||
FindLinkByChildUserID(ctx context.Context, childUserID string) (*entities.UserSubordinateLink, error)
|
||||
FindLinkByParentAndChild(ctx context.Context, parentUserID, childUserID string) (*entities.UserSubordinateLink, error)
|
||||
ListChildrenByParent(ctx context.Context, parentUserID string, limit, offset int) ([]*entities.UserSubordinateLink, int64, error)
|
||||
UpdateLink(ctx context.Context, link *entities.UserSubordinateLink) error
|
||||
// 是否存在子账号关系(任意子账号)
|
||||
IsUserSubordinate(ctx context.Context, userID string) (bool, error)
|
||||
|
||||
// 划拨
|
||||
CreateWalletAllocation(ctx context.Context, a *entities.SubordinateWalletAllocation) error
|
||||
ListWalletAllocationsByParentAndChild(ctx context.Context, parentUserID, childUserID string, limit, offset int) ([]*entities.SubordinateWalletAllocation, int64, error)
|
||||
|
||||
// 额度购买
|
||||
CreateQuotaPurchase(ctx context.Context, p *entities.SubordinateQuotaPurchase) error
|
||||
ListQuotaPurchasesByParentAndChild(ctx context.Context, parentUserID, childUserID string, limit, offset int) ([]*entities.SubordinateQuotaPurchase, int64, error)
|
||||
|
||||
// 额度账户
|
||||
FindQuotaAccount(ctx context.Context, userID, productID string) (*entities.UserProductQuotaAccount, error)
|
||||
CreateQuotaAccount(ctx context.Context, account *entities.UserProductQuotaAccount) error
|
||||
UpdateQuotaAccount(ctx context.Context, account *entities.UserProductQuotaAccount) error
|
||||
ListQuotaAccountsByUser(ctx context.Context, userID string) ([]*entities.UserProductQuotaAccount, error)
|
||||
CreateQuotaLedger(ctx context.Context, ledger *entities.UserProductQuotaLedger) error
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package certification
|
||||
import (
|
||||
"context"
|
||||
"tyapi-server/internal/domains/certification/entities"
|
||||
"tyapi-server/internal/domains/certification/repositories"
|
||||
"tyapi-server/internal/shared/database"
|
||||
|
||||
"go.uber.org/zap"
|
||||
@@ -39,6 +40,15 @@ func (r *GormEnterpriseInfoSubmitRecordRepository) Exists(ctx context.Context, I
|
||||
return r.ExistsEntity(ctx, ID, &entities.EnterpriseInfoSubmitRecord{})
|
||||
}
|
||||
|
||||
func (r *GormEnterpriseInfoSubmitRecordRepository) FindByID(ctx context.Context, id string) (*entities.EnterpriseInfoSubmitRecord, error) {
|
||||
var record entities.EnterpriseInfoSubmitRecord
|
||||
err := r.GetDB(ctx).Where("id = ?", id).First(&record).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
func (r *GormEnterpriseInfoSubmitRecordRepository) FindLatestByUserID(ctx context.Context, userID string) (*entities.EnterpriseInfoSubmitRecord, error) {
|
||||
var record entities.EnterpriseInfoSubmitRecord
|
||||
err := r.GetDB(ctx).
|
||||
@@ -61,4 +71,69 @@ func (r *GormEnterpriseInfoSubmitRecordRepository) FindLatestVerifiedByUserID(ct
|
||||
return nil, err
|
||||
}
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
// ExistsByUnifiedSocialCodeExcludeUser 检查该统一社会信用代码是否已被其他用户占用(已提交或已通过验证的记录)
|
||||
func (r *GormEnterpriseInfoSubmitRecordRepository) ExistsByUnifiedSocialCodeExcludeUser(ctx context.Context, unifiedSocialCode string, excludeUserID string) (bool, error) {
|
||||
if unifiedSocialCode == "" {
|
||||
return false, nil
|
||||
}
|
||||
var count int64
|
||||
query := r.GetDB(ctx).Model(&entities.EnterpriseInfoSubmitRecord{}).
|
||||
Where("unified_social_code = ? AND status IN (?, ?)", unifiedSocialCode, "submitted", "verified")
|
||||
if excludeUserID != "" {
|
||||
query = query.Where("user_id != ?", excludeUserID)
|
||||
}
|
||||
if err := query.Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (r *GormEnterpriseInfoSubmitRecordRepository) List(ctx context.Context, filter repositories.ListSubmitRecordsFilter) (*repositories.ListSubmitRecordsResult, error) {
|
||||
base := r.GetDB(ctx).Model(&entities.EnterpriseInfoSubmitRecord{})
|
||||
if filter.CertificationStatus != "" {
|
||||
base = base.Joins("JOIN certifications ON certifications.user_id = enterprise_info_submit_records.user_id AND certifications.deleted_at IS NULL").
|
||||
Where("certifications.status = ?", filter.CertificationStatus)
|
||||
}
|
||||
if filter.CompanyName != "" {
|
||||
base = base.Where("enterprise_info_submit_records.company_name LIKE ?", "%"+filter.CompanyName+"%")
|
||||
}
|
||||
if filter.LegalPersonPhone != "" {
|
||||
base = base.Where("enterprise_info_submit_records.legal_person_phone = ?", filter.LegalPersonPhone)
|
||||
}
|
||||
if filter.LegalPersonName != "" {
|
||||
base = base.Where("enterprise_info_submit_records.legal_person_name LIKE ?", "%"+filter.LegalPersonName+"%")
|
||||
}
|
||||
var total int64
|
||||
if err := base.Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if filter.PageSize <= 0 {
|
||||
filter.PageSize = 10
|
||||
}
|
||||
if filter.Page <= 0 {
|
||||
filter.Page = 1
|
||||
}
|
||||
offset := (filter.Page - 1) * filter.PageSize
|
||||
var records []*entities.EnterpriseInfoSubmitRecord
|
||||
q := r.GetDB(ctx).Model(&entities.EnterpriseInfoSubmitRecord{})
|
||||
if filter.CertificationStatus != "" {
|
||||
q = q.Joins("JOIN certifications ON certifications.user_id = enterprise_info_submit_records.user_id AND certifications.deleted_at IS NULL").
|
||||
Where("certifications.status = ?", filter.CertificationStatus)
|
||||
}
|
||||
if filter.CompanyName != "" {
|
||||
q = q.Where("enterprise_info_submit_records.company_name LIKE ?", "%"+filter.CompanyName+"%")
|
||||
}
|
||||
if filter.LegalPersonPhone != "" {
|
||||
q = q.Where("enterprise_info_submit_records.legal_person_phone = ?", filter.LegalPersonPhone)
|
||||
}
|
||||
if filter.LegalPersonName != "" {
|
||||
q = q.Where("enterprise_info_submit_records.legal_person_name LIKE ?", "%"+filter.LegalPersonName+"%")
|
||||
}
|
||||
err := q.Order("enterprise_info_submit_records.submit_at DESC").Offset(offset).Limit(filter.PageSize).Find(&records).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &repositories.ListSubmitRecordsResult{Records: records, Total: total}, nil
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
package subordinate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"tyapi-server/internal/domains/subordinate/entities"
|
||||
"tyapi-server/internal/domains/subordinate/repositories"
|
||||
shared_database "tyapi-server/internal/shared/database"
|
||||
)
|
||||
|
||||
// GormSubordinateRepository 下属模块 GORM 实现
|
||||
type GormSubordinateRepository struct {
|
||||
db *gorm.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
var _ repositories.SubordinateRepository = (*GormSubordinateRepository)(nil)
|
||||
|
||||
// NewGormSubordinateRepository 构造
|
||||
func NewGormSubordinateRepository(db *gorm.DB, logger *zap.Logger) *GormSubordinateRepository {
|
||||
return &GormSubordinateRepository{db: db, logger: logger}
|
||||
}
|
||||
|
||||
func (r *GormSubordinateRepository) withCtx(ctx context.Context) *gorm.DB {
|
||||
if tx, ok := shared_database.GetTx(ctx); ok {
|
||||
return tx.WithContext(ctx)
|
||||
}
|
||||
return r.db.WithContext(ctx)
|
||||
}
|
||||
|
||||
// CreateInvitation 创建邀请
|
||||
func (r *GormSubordinateRepository) CreateInvitation(ctx context.Context, inv *entities.SubordinateInvitation) error {
|
||||
return r.withCtx(ctx).Create(inv).Error
|
||||
}
|
||||
|
||||
// FindInvitationByTokenHash 按 token 哈希查询
|
||||
func (r *GormSubordinateRepository) FindInvitationByTokenHash(ctx context.Context, tokenHash string) (*entities.SubordinateInvitation, error) {
|
||||
var inv entities.SubordinateInvitation
|
||||
err := r.withCtx(ctx).Where("token_hash = ?", tokenHash).First(&inv).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &inv, nil
|
||||
}
|
||||
|
||||
// FindInvitationByID 按ID
|
||||
func (r *GormSubordinateRepository) FindInvitationByID(ctx context.Context, id string) (*entities.SubordinateInvitation, error) {
|
||||
var inv entities.SubordinateInvitation
|
||||
err := r.withCtx(ctx).Where("id = ?", id).First(&inv).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &inv, nil
|
||||
}
|
||||
|
||||
// UpdateInvitation 更新
|
||||
func (r *GormSubordinateRepository) UpdateInvitation(ctx context.Context, inv *entities.SubordinateInvitation) error {
|
||||
return r.withCtx(ctx).Save(inv).Error
|
||||
}
|
||||
|
||||
// ConsumeInvitation 原子核销邀请(仅 pending 可核销)
|
||||
func (r *GormSubordinateRepository) ConsumeInvitation(ctx context.Context, invitationID, childUserID string, consumedAt time.Time) (bool, error) {
|
||||
uid := childUserID
|
||||
res := r.withCtx(ctx).
|
||||
Model(&entities.SubordinateInvitation{}).
|
||||
Where("id = ? AND status = ?", invitationID, entities.InvitationStatusPending).
|
||||
Updates(map[string]interface{}{
|
||||
"status": entities.InvitationStatusConsumed,
|
||||
"consumed_by_user_id": &uid,
|
||||
"consumed_at": &consumedAt,
|
||||
})
|
||||
if res.Error != nil {
|
||||
return false, res.Error
|
||||
}
|
||||
return res.RowsAffected > 0, nil
|
||||
}
|
||||
|
||||
// ListInvitationsByParent 主账号邀请列表
|
||||
func (r *GormSubordinateRepository) ListInvitationsByParent(ctx context.Context, parentUserID string, limit, offset int) ([]*entities.SubordinateInvitation, int64, error) {
|
||||
var list []entities.SubordinateInvitation
|
||||
var total int64
|
||||
q := r.withCtx(ctx).Model(&entities.SubordinateInvitation{}).Where("parent_user_id = ?", parentUserID)
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := q.Order("created_at DESC").Limit(limit).Offset(offset).Find(&list).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
out := make([]*entities.SubordinateInvitation, len(list))
|
||||
for i := range list {
|
||||
out[i] = &list[i]
|
||||
}
|
||||
return out, total, nil
|
||||
}
|
||||
|
||||
// CreateLink 创建主从
|
||||
func (r *GormSubordinateRepository) CreateLink(ctx context.Context, link *entities.UserSubordinateLink) error {
|
||||
return r.withCtx(ctx).Create(link).Error
|
||||
}
|
||||
|
||||
// FindLinkByChildUserID 按子查
|
||||
func (r *GormSubordinateRepository) FindLinkByChildUserID(ctx context.Context, childUserID string) (*entities.UserSubordinateLink, error) {
|
||||
var l entities.UserSubordinateLink
|
||||
err := r.withCtx(ctx).Where("child_user_id = ? AND status = ?", childUserID, entities.LinkStatusActive).First(&l).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &l, nil
|
||||
}
|
||||
|
||||
// FindLinkByParentAndChild 精确查
|
||||
func (r *GormSubordinateRepository) FindLinkByParentAndChild(ctx context.Context, parentUserID, childUserID string) (*entities.UserSubordinateLink, error) {
|
||||
var l entities.UserSubordinateLink
|
||||
err := r.withCtx(ctx).Where("parent_user_id = ? AND child_user_id = ?", parentUserID, childUserID).First(&l).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &l, nil
|
||||
}
|
||||
|
||||
// ListChildrenByParent 列出下属
|
||||
func (r *GormSubordinateRepository) ListChildrenByParent(ctx context.Context, parentUserID string, limit, offset int) ([]*entities.UserSubordinateLink, int64, error) {
|
||||
var list []entities.UserSubordinateLink
|
||||
var total int64
|
||||
q := r.withCtx(ctx).Model(&entities.UserSubordinateLink{}).Where("parent_user_id = ? AND status = ?", parentUserID, entities.LinkStatusActive)
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := q.Order("created_at DESC").Limit(limit).Offset(offset).Find(&list).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
out := make([]*entities.UserSubordinateLink, len(list))
|
||||
for i := range list {
|
||||
out[i] = &list[i]
|
||||
}
|
||||
return out, total, nil
|
||||
}
|
||||
|
||||
// UpdateLink 更新
|
||||
func (r *GormSubordinateRepository) UpdateLink(ctx context.Context, link *entities.UserSubordinateLink) error {
|
||||
return r.withCtx(ctx).Save(link).Error
|
||||
}
|
||||
|
||||
// IsUserSubordinate 是否为主账号的下属(存在 active 的 child 记录)
|
||||
func (r *GormSubordinateRepository) IsUserSubordinate(ctx context.Context, userID string) (bool, error) {
|
||||
var n int64
|
||||
err := r.withCtx(ctx).Model(&entities.UserSubordinateLink{}).Where("child_user_id = ? AND status = ?", userID, entities.LinkStatusActive).Count(&n).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return n > 0, nil
|
||||
}
|
||||
|
||||
// CreateWalletAllocation 记划拨
|
||||
func (r *GormSubordinateRepository) CreateWalletAllocation(ctx context.Context, a *entities.SubordinateWalletAllocation) error {
|
||||
// 幂等:同 business_ref 不重复
|
||||
var cnt int64
|
||||
if err := r.withCtx(ctx).Model(&entities.SubordinateWalletAllocation{}).Where("business_ref = ?", a.BusinessRef).Count(&cnt).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if cnt > 0 {
|
||||
return fmt.Errorf("划拨记录已存在")
|
||||
}
|
||||
return r.withCtx(ctx).Create(a).Error
|
||||
}
|
||||
|
||||
// ListWalletAllocationsByParentAndChild 查询主对子划拨记录
|
||||
func (r *GormSubordinateRepository) ListWalletAllocationsByParentAndChild(ctx context.Context, parentUserID, childUserID string, limit, offset int) ([]*entities.SubordinateWalletAllocation, int64, error) {
|
||||
var list []entities.SubordinateWalletAllocation
|
||||
var total int64
|
||||
q := r.withCtx(ctx).Model(&entities.SubordinateWalletAllocation{}).Where("from_user_id = ? AND to_user_id = ?", parentUserID, childUserID)
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := q.Order("created_at DESC").Limit(limit).Offset(offset).Find(&list).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
out := make([]*entities.SubordinateWalletAllocation, len(list))
|
||||
for i := range list {
|
||||
out[i] = &list[i]
|
||||
}
|
||||
return out, total, nil
|
||||
}
|
||||
|
||||
// CreateQuotaPurchase 创建额度购买记录
|
||||
func (r *GormSubordinateRepository) CreateQuotaPurchase(ctx context.Context, p *entities.SubordinateQuotaPurchase) error {
|
||||
return r.withCtx(ctx).Create(p).Error
|
||||
}
|
||||
|
||||
// ListQuotaPurchasesByParentAndChild 查询主对子额度购买记录
|
||||
func (r *GormSubordinateRepository) ListQuotaPurchasesByParentAndChild(ctx context.Context, parentUserID, childUserID string, limit, offset int) ([]*entities.SubordinateQuotaPurchase, int64, error) {
|
||||
var list []entities.SubordinateQuotaPurchase
|
||||
var total int64
|
||||
q := r.withCtx(ctx).Model(&entities.SubordinateQuotaPurchase{}).Where("parent_user_id = ? AND child_user_id = ?", parentUserID, childUserID)
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := q.Order("created_at DESC").Limit(limit).Offset(offset).Find(&list).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
out := make([]*entities.SubordinateQuotaPurchase, len(list))
|
||||
for i := range list {
|
||||
out[i] = &list[i]
|
||||
}
|
||||
return out, total, nil
|
||||
}
|
||||
|
||||
// FindQuotaAccount 查询用户产品额度账户
|
||||
func (r *GormSubordinateRepository) FindQuotaAccount(ctx context.Context, userID, productID string) (*entities.UserProductQuotaAccount, error) {
|
||||
var account entities.UserProductQuotaAccount
|
||||
err := r.withCtx(ctx).Where("user_id = ? AND product_id = ?", userID, productID).First(&account).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
// CreateQuotaAccount 创建额度账户
|
||||
func (r *GormSubordinateRepository) CreateQuotaAccount(ctx context.Context, account *entities.UserProductQuotaAccount) error {
|
||||
return r.withCtx(ctx).Create(account).Error
|
||||
}
|
||||
|
||||
// UpdateQuotaAccount 更新额度账户
|
||||
func (r *GormSubordinateRepository) UpdateQuotaAccount(ctx context.Context, account *entities.UserProductQuotaAccount) error {
|
||||
return r.withCtx(ctx).Save(account).Error
|
||||
}
|
||||
|
||||
// ListQuotaAccountsByUser 查询用户全部额度账户
|
||||
func (r *GormSubordinateRepository) ListQuotaAccountsByUser(ctx context.Context, userID string) ([]*entities.UserProductQuotaAccount, error) {
|
||||
var list []entities.UserProductQuotaAccount
|
||||
if err := r.withCtx(ctx).Where("user_id = ?", userID).Order("updated_at DESC").Find(&list).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*entities.UserProductQuotaAccount, len(list))
|
||||
for i := range list {
|
||||
out[i] = &list[i]
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// CreateQuotaLedger 创建额度流水
|
||||
func (r *GormSubordinateRepository) CreateQuotaLedger(ctx context.Context, ledger *entities.UserProductQuotaLedger) error {
|
||||
return r.withCtx(ctx).Create(ledger).Error
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"tyapi-server/internal/domains/user/entities"
|
||||
"tyapi-server/internal/domains/user/repositories"
|
||||
@@ -107,7 +108,48 @@ func (r *GormUserRepository) ExistsByUnifiedSocialCode(ctx context.Context, unif
|
||||
}
|
||||
|
||||
func (r *GormUserRepository) Update(ctx context.Context, user entities.User) error {
|
||||
return r.UpdateEntity(ctx, &user)
|
||||
db := r.GetDB(ctx)
|
||||
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
// 避免 GORM 自动保存关联触发 ON CONFLICT(受历史库索引差异影响)
|
||||
if err := tx.WithContext(ctx).Omit(clause.Associations).Save(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 企业信息单独按 user_id 做更新或创建,避免关联 upsert 依赖冲突约束
|
||||
if user.EnterpriseInfo == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
enterpriseInfo := *user.EnterpriseInfo
|
||||
enterpriseInfo.UserID = user.ID
|
||||
enterpriseInfo.User = nil
|
||||
|
||||
var count int64
|
||||
if err := tx.WithContext(ctx).
|
||||
Model(&entities.EnterpriseInfo{}).
|
||||
Where("user_id = ?", user.ID).
|
||||
Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
updates := map[string]interface{}{
|
||||
"company_name": enterpriseInfo.CompanyName,
|
||||
"unified_social_code": enterpriseInfo.UnifiedSocialCode,
|
||||
"legal_person_name": enterpriseInfo.LegalPersonName,
|
||||
"legal_person_id": enterpriseInfo.LegalPersonID,
|
||||
"legal_person_phone": enterpriseInfo.LegalPersonPhone,
|
||||
"enterprise_address": enterpriseInfo.EnterpriseAddress,
|
||||
}
|
||||
return tx.WithContext(ctx).
|
||||
Model(&entities.EnterpriseInfo{}).
|
||||
Where("user_id = ?", user.ID).
|
||||
Updates(updates).Error
|
||||
}
|
||||
|
||||
return tx.WithContext(ctx).Create(&enterpriseInfo).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *GormUserRepository) CreateBatch(ctx context.Context, users []entities.User) error {
|
||||
|
||||
49
internal/infrastructure/external/alicloud/alicloud_factory.go
vendored
Normal file
49
internal/infrastructure/external/alicloud/alicloud_factory.go
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
package alicloud
|
||||
|
||||
import (
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
// NewAlicloudServiceWithConfig 使用配置创建阿里云服务,并启用外部服务调用日志
|
||||
func NewAlicloudServiceWithConfig(cfg *config.Config) (*AlicloudService, error) {
|
||||
loggingConfig := external_logger.ExternalServiceLoggingConfig{
|
||||
Enabled: true,
|
||||
LogDir: "./logs/external_services",
|
||||
ServiceName: "alicloud",
|
||||
UseDaily: false,
|
||||
EnableLevelSeparation: true,
|
||||
LevelConfigs: map[string]external_logger.ExternalServiceLevelFileConfig{
|
||||
"info": {
|
||||
MaxSize: 100,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 28,
|
||||
Compress: true,
|
||||
},
|
||||
"error": {
|
||||
MaxSize: 100,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 28,
|
||||
Compress: true,
|
||||
},
|
||||
"warn": {
|
||||
MaxSize: 100,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 28,
|
||||
Compress: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewAlicloudService(
|
||||
cfg.Alicloud.Host,
|
||||
cfg.Alicloud.AppCode,
|
||||
logger,
|
||||
), nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package alicloud
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -8,6 +9,8 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -24,25 +27,47 @@ type AlicloudConfig struct {
|
||||
// AlicloudService 阿里云服务
|
||||
type AlicloudService struct {
|
||||
config AlicloudConfig
|
||||
logger *external_logger.ExternalServiceLogger
|
||||
}
|
||||
|
||||
// NewAlicloudService 创建阿里云服务实例
|
||||
func NewAlicloudService(host, appCode string) *AlicloudService {
|
||||
func NewAlicloudService(host, appCode string, logger ...*external_logger.ExternalServiceLogger) *AlicloudService {
|
||||
var serviceLogger *external_logger.ExternalServiceLogger
|
||||
if len(logger) > 0 {
|
||||
serviceLogger = logger[0]
|
||||
}
|
||||
return &AlicloudService{
|
||||
config: AlicloudConfig{
|
||||
Host: host,
|
||||
AppCode: appCode,
|
||||
},
|
||||
logger: serviceLogger,
|
||||
}
|
||||
}
|
||||
|
||||
// generateRequestID 生成请求ID
|
||||
func (a *AlicloudService) generateRequestID() string {
|
||||
timestamp := time.Now().UnixNano()
|
||||
hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", timestamp, a.config.Host)))
|
||||
return fmt.Sprintf("alicloud_%x", hash[:8])
|
||||
}
|
||||
|
||||
// CallAPI 调用阿里云API的通用方法
|
||||
// path: API路径(如 "api-mall/api/id_card/check")
|
||||
// params: 请求参数
|
||||
func (a *AlicloudService) CallAPI(path string, params map[string]interface{}) (respBytes []byte, err error) {
|
||||
startTime := time.Now()
|
||||
requestID := a.generateRequestID()
|
||||
transactionID := ""
|
||||
|
||||
// 构建请求URL
|
||||
reqURL := a.config.Host + "/" + path
|
||||
|
||||
// 记录请求日志
|
||||
if a.logger != nil {
|
||||
a.logger.LogRequest(requestID, transactionID, path, reqURL)
|
||||
}
|
||||
|
||||
// 构建请求参数
|
||||
formData := url.Values{}
|
||||
for key, value := range params {
|
||||
@@ -52,6 +77,9 @@ func (a *AlicloudService) CallAPI(path string, params map[string]interface{}) (r
|
||||
// 创建HTTP请求
|
||||
req, err := http.NewRequest("POST", reqURL, strings.NewReader(formData.Encode()))
|
||||
if err != nil {
|
||||
if a.logger != nil {
|
||||
a.logger.LogError(requestID, transactionID, path, errors.Join(ErrSystem, err), params)
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, err.Error())
|
||||
}
|
||||
|
||||
@@ -69,17 +97,22 @@ func (a *AlicloudService) CallAPI(path string, params map[string]interface{}) (r
|
||||
isTimeout := false
|
||||
if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() {
|
||||
isTimeout = true
|
||||
} else if errStr := err.Error();
|
||||
errStr == "context deadline exceeded" ||
|
||||
errStr == "timeout" ||
|
||||
errStr == "Client.Timeout exceeded" ||
|
||||
errStr == "net/http: request canceled" {
|
||||
} else if errStr := err.Error(); errStr == "context deadline exceeded" ||
|
||||
errStr == "timeout" ||
|
||||
errStr == "Client.Timeout exceeded" ||
|
||||
errStr == "net/http: request canceled" {
|
||||
isTimeout = true
|
||||
}
|
||||
|
||||
|
||||
if isTimeout {
|
||||
if a.logger != nil {
|
||||
a.logger.LogError(requestID, transactionID, path, errors.Join(ErrDatasource, fmt.Errorf("API请求超时: %s", err.Error())), params)
|
||||
}
|
||||
return nil, fmt.Errorf("%w: API请求超时: %s", ErrDatasource, err.Error())
|
||||
}
|
||||
if a.logger != nil {
|
||||
a.logger.LogError(requestID, transactionID, path, errors.Join(ErrSystem, err), params)
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, err.Error())
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
@@ -87,9 +120,18 @@ func (a *AlicloudService) CallAPI(path string, params map[string]interface{}) (r
|
||||
// 读取响应体
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
if a.logger != nil {
|
||||
a.logger.LogError(requestID, transactionID, path, errors.Join(ErrSystem, err), params)
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, err.Error())
|
||||
}
|
||||
|
||||
// 记录响应日志(不记录具体响应数据)
|
||||
if a.logger != nil {
|
||||
duration := time.Since(startTime)
|
||||
a.logger.LogResponse(requestID, transactionID, path, resp.StatusCode, duration)
|
||||
}
|
||||
|
||||
// 直接返回原始响应body,让调用方自己处理
|
||||
return body, nil
|
||||
}
|
||||
@@ -97,4 +139,4 @@ func (a *AlicloudService) CallAPI(path string, params map[string]interface{}) (r
|
||||
// GetConfig 获取配置信息
|
||||
func (a *AlicloudService) GetConfig() AlicloudConfig {
|
||||
return a.config
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user