Compare commits
25 Commits
e67465a58d
...
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 |
37
config.yaml
37
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"
|
||||
@@ -131,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
|
||||
@@ -206,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 # 是否启用地理位置阻止
|
||||
@@ -237,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"
|
||||
|
||||
@@ -399,6 +401,7 @@ WechatH5:
|
||||
# ===========================================
|
||||
# 🔍 天眼查配置
|
||||
# ===========================================
|
||||
|
||||
tianyancha:
|
||||
base_url: http://open.api.tianyancha.com/services
|
||||
api_key: e6a43dc9-786e-4a16-bb12-392b8201d8e2
|
||||
@@ -548,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表示不限制,单位:字节)
|
||||
|
||||
# ===========================================
|
||||
# ✨ 数脉配置走实时接口
|
||||
@@ -606,7 +609,6 @@ shumai:
|
||||
max_age: 30
|
||||
compress: true
|
||||
|
||||
|
||||
# ===========================================
|
||||
# ✨ 数据宝配置走实时接口
|
||||
# ===========================================
|
||||
@@ -639,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"
|
||||
|
||||
# ===========================================
|
||||
# 🗄️ 数据库配置
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -157,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 # 启用代理检查
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
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"
|
||||
)
|
||||
@@ -264,6 +265,14 @@ func (a *Application) autoMigrate(db *gorm.DB) error {
|
||||
&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
|
||||
@@ -1099,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),
|
||||
@@ -1192,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 {
|
||||
@@ -1210,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),
|
||||
@@ -1237,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,
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"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"
|
||||
@@ -18,7 +19,10 @@ import (
|
||||
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"
|
||||
@@ -49,6 +53,8 @@ type CertificationApplicationServiceImpl struct {
|
||||
// 仓储依赖
|
||||
queryRepository repositories.CertificationQueryRepository
|
||||
enterpriseInfoSubmitRecordRepo repositories.EnterpriseInfoSubmitRecordRepository
|
||||
subordinateRepo subordinate_repositories.SubordinateRepository
|
||||
walletRepo finance_repositories.WalletRepository
|
||||
txManager *database.TransactionManager
|
||||
|
||||
wechatWorkService *notification.WeChatWorkService
|
||||
@@ -71,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,
|
||||
@@ -93,6 +101,8 @@ func NewCertificationApplicationService(
|
||||
apiUserAggregateService: apiUserAggregateService,
|
||||
enterpriseInfoSubmitRecordService: enterpriseInfoSubmitRecordService,
|
||||
ocrService: ocrService,
|
||||
subordinateRepo: subordinateRepo,
|
||||
walletRepo: walletRepo,
|
||||
txManager: txManager,
|
||||
wechatWorkService: wechatSvc,
|
||||
logger: logger,
|
||||
@@ -1632,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))
|
||||
}
|
||||
|
||||
|
||||
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. 添加企业信息
|
||||
|
||||
@@ -197,6 +197,8 @@ 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配置
|
||||
|
||||
@@ -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"
|
||||
@@ -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,
|
||||
@@ -1242,6 +1275,7 @@ func NewContainer() *Container {
|
||||
fx.Provide(
|
||||
// 用户HTTP处理器
|
||||
handlers.NewUserHandler,
|
||||
handlers.NewSubordinateHandler,
|
||||
// 认证HTTP处理器
|
||||
handlers.NewCertificationHandler,
|
||||
// 财务HTTP处理器
|
||||
@@ -1325,6 +1359,7 @@ func NewContainer() *Container {
|
||||
fx.Provide(
|
||||
// 用户路由
|
||||
routes.NewUserRoutes,
|
||||
routes.NewSubordinateRoutes,
|
||||
// 验证码路由
|
||||
routes.NewCaptchaRoutes,
|
||||
// 认证路由
|
||||
@@ -1457,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,
|
||||
@@ -1484,6 +1520,7 @@ func RegisterRoutes(
|
||||
|
||||
// 所有域名路由路由
|
||||
userRoutes.Register(router)
|
||||
subordinateRoutes.Register(router)
|
||||
captchaRoutes.Register(router)
|
||||
certificationRoutes.Register(router)
|
||||
financeRoutes.Register(router)
|
||||
|
||||
@@ -73,7 +73,7 @@ type IVYZ81NCReq struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
}
|
||||
type IVYZ2MN6Req struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
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"`
|
||||
}
|
||||
@@ -499,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"`
|
||||
@@ -665,6 +671,11 @@ type QYGLDJ12Req struct {
|
||||
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"`
|
||||
@@ -1048,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"`
|
||||
|
||||
@@ -248,6 +248,7 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"QYGLUY3S": qygl.ProcessQYGLUY3SRequest, //企业经营状态全景查询
|
||||
"QYGLDJ12": qygl.ProcessQYGLDJ12Request, //企业年报信息核验
|
||||
"QYGL8848": qygl.ProcessQYGL8848Request, //企业税收违法核查
|
||||
"QYGLDJ33": qygl.ProcessQYGLDJ33Request, //企业年报信息核验
|
||||
|
||||
// YYSY系列处理器
|
||||
"YYSY35TA": yysy.ProcessYYSY35TARequest, //运营商归属地数卖
|
||||
@@ -318,6 +319,7 @@ 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, //活体识别步骤二
|
||||
@@ -328,6 +330,9 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"IVYZ38SR": ivyz.ProcessIVYZ38SRRequest, //婚姻状态核验(双人)
|
||||
"IVYZ48SR": ivyz.ProcessIVYZ48SRRequest, //婚姻状态核验V2(双人)
|
||||
"IVYZ5E22": ivyz.ProcessIVYZ5E22Request, //双人婚姻评估查询zhicha版本
|
||||
"IVYZRAX1": ivyz.ProcessIVYZRAX1Request, //融安信用分
|
||||
"IVYZRAX2": ivyz.ProcessIVYZRAX2Request,//融御反欺诈分
|
||||
|
||||
|
||||
// COMB系列处理器 - 只注册有自定义逻辑的组合包
|
||||
"COMB86PM": comb.ProcessCOMB86PMRequest, // 有自定义逻辑:重命名ApiCode
|
||||
|
||||
@@ -241,6 +241,7 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
|
||||
"YYSYK8R3": &dto.YYSYK8R3Req{}, //手机空号检测查询
|
||||
"YYSYF2T7": &dto.YYSYF2T7Req{}, //手机二次放号检测查询
|
||||
"IVYZA1B3": &dto.IVYZA1B3Req{}, //公安三要素人脸识别
|
||||
"IVYZFIC1": &dto.IVYZFIC1Req{}, //人脸身份证比对(数脉)
|
||||
"IVYZX5QZ": &dto.IVYZX5QZReq{}, //活体识别
|
||||
"IVYZN2P8": &dto.IVYZ9K7FReq{}, //身份证实名认证政务版
|
||||
"YYSYH6F3": &dto.YYSYH6F3Req{}, //运营商三要素简版即时版查询
|
||||
@@ -274,6 +275,9 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
|
||||
"IVYZ48SR": &dto.IVYZ48SRReq{}, //婚姻状态核验V2(双人)
|
||||
"IVYZ5E22": &dto.IVYZ5E22Req{}, //双人婚姻评估查询zhicha版本
|
||||
"DWBG5SAM": &dto.DWBG5SAMReq{}, //天远指迷报告
|
||||
"QYGLDJ33": &dto.QYGLDJ33Req{}, //企业年报信息核验
|
||||
"IVYZRAX1": &dto.IVYZRAX1Req{},//融安信用分
|
||||
"IVYZRAX2": &dto.IVYZRAX1Req{},//融御反欺诈
|
||||
}
|
||||
|
||||
// 优先返回已配置的DTO
|
||||
@@ -635,7 +639,7 @@ func (s *FormConfigServiceImpl) generatePlaceholder(jsonTag string, fieldType st
|
||||
"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": "请输入行驶证图片地址",
|
||||
"image_url": "请输入入参图片地址",
|
||||
"reg_url": "请输入车辆登记证图片地址",
|
||||
"token": "请输入token",
|
||||
"vehicle_name": "请输入车型名称",
|
||||
|
||||
@@ -25,7 +25,7 @@ func ProcessFLXG0V4BRequest(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
// 去掉司法案件案件去掉身份证号码
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" {
|
||||
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,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" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" {
|
||||
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" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" {
|
||||
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,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" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" {
|
||||
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" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" {
|
||||
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,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,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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -4,9 +4,11 @@ import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -119,3 +121,10 @@ func pkcs7Unpadding(src []byte) ([]byte, error) {
|
||||
|
||||
return src[:length-unpadding], nil
|
||||
}
|
||||
|
||||
// MD5 使用MD5加密数据,返回十六进制字符串
|
||||
func MD5(data string) string {
|
||||
h := md5.New()
|
||||
io.WriteString(h, data)
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
@@ -212,11 +212,12 @@ func (z *ZhichaService) CallAPI(ctx context.Context, proID string, params map[st
|
||||
|
||||
// 201 表示查询为空,兼容其它情况如果data也为空,则返回空对象
|
||||
if zhichaResp.Code == "201" {
|
||||
// 先做类型断言
|
||||
dataMap, ok := zhichaResp.Data.(map[string]interface{})
|
||||
if ok && len(dataMap) > 0 {
|
||||
if ok {
|
||||
// 即使是 {},也原样返回
|
||||
return dataMap, nil
|
||||
}
|
||||
// 兜底:防止解密异常
|
||||
return map[string]interface{}{}, nil
|
||||
}
|
||||
|
||||
@@ -315,6 +316,12 @@ func (z *ZhichaService) Decrypt(encryptedData string) (string, error) {
|
||||
return string(unpadded), nil
|
||||
}
|
||||
|
||||
// MD5 对字符串进行MD5加密并返回32位小写十六进制字符串
|
||||
func (z *ZhichaService) MD5(data string) string {
|
||||
hash := md5.Sum([]byte(data))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// pkcs7Padding 使用PKCS7填充数据
|
||||
func (z *ZhichaService) pkcs7Padding(src []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(src)%blockSize
|
||||
|
||||
299
internal/infrastructure/http/handlers/subordinate_handler.go
Normal file
299
internal/infrastructure/http/handlers/subordinate_handler.go
Normal file
@@ -0,0 +1,299 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
subordinate_app "tyapi-server/internal/application/subordinate"
|
||||
"tyapi-server/internal/application/subordinate/dto/commands"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// SubordinateHandler 下属账号
|
||||
type SubordinateHandler struct {
|
||||
app subordinate_app.SubordinateApplicationService
|
||||
response interfaces.ResponseBuilder
|
||||
validator interfaces.RequestValidator
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewSubordinateHandler 构造
|
||||
func NewSubordinateHandler(
|
||||
app subordinate_app.SubordinateApplicationService,
|
||||
response interfaces.ResponseBuilder,
|
||||
validator interfaces.RequestValidator,
|
||||
logger *zap.Logger,
|
||||
) *SubordinateHandler {
|
||||
return &SubordinateHandler{app: app, response: response, validator: validator, logger: logger}
|
||||
}
|
||||
|
||||
// SubPortalRegister 子站注册
|
||||
func (h *SubordinateHandler) SubPortalRegister(c *gin.Context) {
|
||||
var cmd commands.SubPortalRegisterCommand
|
||||
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
res, err := h.app.RegisterSubPortal(c.Request.Context(), &cmd)
|
||||
if err != nil {
|
||||
h.logger.Error("子站注册失败", zap.Error(err))
|
||||
h.response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
h.response.Created(c, res, "注册成功")
|
||||
}
|
||||
|
||||
// CreateInvitation 主账号创建邀请
|
||||
func (h *SubordinateHandler) CreateInvitation(c *gin.Context) {
|
||||
parentID := c.GetString("user_id")
|
||||
if parentID == "" {
|
||||
h.response.Unauthorized(c, "未登录")
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
ExpiresInHours int `json:"expires_in_hours"`
|
||||
}
|
||||
_ = c.ShouldBindJSON(&body)
|
||||
res, err := h.app.CreateInvitation(c.Request.Context(), &commands.CreateInvitationCommand{
|
||||
ParentUserID: parentID,
|
||||
ExpiresInHours: body.ExpiresInHours,
|
||||
})
|
||||
if err != nil {
|
||||
h.logger.Error("创建邀请失败", zap.Error(err))
|
||||
h.response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
h.response.Success(c, res, "邀请已创建")
|
||||
}
|
||||
|
||||
// ListSubordinates 下属列表
|
||||
func (h *SubordinateHandler) ListSubordinates(c *gin.Context) {
|
||||
parentID := c.GetString("user_id")
|
||||
if parentID == "" {
|
||||
h.response.Unauthorized(c, "未登录")
|
||||
return
|
||||
}
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
res, err := h.app.ListMySubordinates(c.Request.Context(), parentID, page, size)
|
||||
if err != nil {
|
||||
h.logger.Error("获取下属列表失败", zap.Error(err))
|
||||
h.response.InternalError(c, "获取下属列表失败")
|
||||
return
|
||||
}
|
||||
h.response.Success(c, res, "获取成功")
|
||||
}
|
||||
|
||||
// Allocate 划款
|
||||
func (h *SubordinateHandler) Allocate(c *gin.Context) {
|
||||
parentID := c.GetString("user_id")
|
||||
if parentID == "" {
|
||||
h.response.Unauthorized(c, "未登录")
|
||||
return
|
||||
}
|
||||
var cmd commands.AllocateToChildCommand
|
||||
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
cmd.ParentUserID = parentID
|
||||
if err := h.app.AllocateToChild(c.Request.Context(), &cmd); err != nil {
|
||||
h.logger.Error("划拨失败", zap.Error(err))
|
||||
h.response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
h.response.Success(c, nil, "划拨成功")
|
||||
}
|
||||
|
||||
// ListAllocations 下属划拨记录
|
||||
func (h *SubordinateHandler) ListAllocations(c *gin.Context) {
|
||||
parentID := c.GetString("user_id")
|
||||
if parentID == "" {
|
||||
h.response.Unauthorized(c, "未登录")
|
||||
return
|
||||
}
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
cmd := &commands.ListChildAllocationsCommand{
|
||||
ParentUserID: parentID,
|
||||
ChildUserID: c.Query("child_user_id"),
|
||||
Page: page,
|
||||
PageSize: size,
|
||||
}
|
||||
if cmd.ChildUserID == "" {
|
||||
h.response.BadRequest(c, "child_user_id 不能为空")
|
||||
return
|
||||
}
|
||||
res, err := h.app.ListChildAllocations(c.Request.Context(), cmd)
|
||||
if err != nil {
|
||||
h.logger.Error("获取划拨记录失败", zap.Error(err))
|
||||
h.response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
h.response.Success(c, res, "获取成功")
|
||||
}
|
||||
|
||||
// AssignSubscription 代配订阅
|
||||
func (h *SubordinateHandler) AssignSubscription(c *gin.Context) {
|
||||
parentID := c.GetString("user_id")
|
||||
if parentID == "" {
|
||||
h.response.Unauthorized(c, "未登录")
|
||||
return
|
||||
}
|
||||
var cmd commands.AssignChildSubscriptionCommand
|
||||
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
cmd.ParentUserID = parentID
|
||||
if err := h.app.AssignChildSubscription(c.Request.Context(), &cmd); err != nil {
|
||||
h.logger.Error("代配订阅失败", zap.Error(err))
|
||||
h.response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
h.response.Success(c, nil, "已保存下属订阅")
|
||||
}
|
||||
|
||||
// ListChildSubscriptions 下属订阅列表
|
||||
func (h *SubordinateHandler) ListChildSubscriptions(c *gin.Context) {
|
||||
parentID := c.GetString("user_id")
|
||||
if parentID == "" {
|
||||
h.response.Unauthorized(c, "未登录")
|
||||
return
|
||||
}
|
||||
childID := c.Query("child_user_id")
|
||||
if childID == "" {
|
||||
h.response.BadRequest(c, "child_user_id 不能为空")
|
||||
return
|
||||
}
|
||||
res, err := h.app.ListChildSubscriptions(c.Request.Context(), &commands.ListChildSubscriptionsCommand{
|
||||
ParentUserID: parentID,
|
||||
ChildUserID: childID,
|
||||
})
|
||||
if err != nil {
|
||||
h.logger.Error("获取下属订阅失败", zap.Error(err))
|
||||
h.response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
h.response.Success(c, res, "获取成功")
|
||||
}
|
||||
|
||||
// RemoveChildSubscription 删除下属订阅
|
||||
func (h *SubordinateHandler) RemoveChildSubscription(c *gin.Context) {
|
||||
parentID := c.GetString("user_id")
|
||||
if parentID == "" {
|
||||
h.response.Unauthorized(c, "未登录")
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
ChildUserID string `json:"child_user_id"`
|
||||
}
|
||||
_ = c.ShouldBindJSON(&body)
|
||||
if body.ChildUserID == "" {
|
||||
h.response.BadRequest(c, "child_user_id 不能为空")
|
||||
return
|
||||
}
|
||||
subID := c.Param("subscription_id")
|
||||
if subID == "" {
|
||||
h.response.BadRequest(c, "subscription_id 不能为空")
|
||||
return
|
||||
}
|
||||
err := h.app.RemoveChildSubscription(c.Request.Context(), &commands.RemoveChildSubscriptionCommand{
|
||||
ParentUserID: parentID,
|
||||
ChildUserID: body.ChildUserID,
|
||||
SubscriptionID: subID,
|
||||
})
|
||||
if err != nil {
|
||||
h.logger.Error("删除下属订阅失败", zap.Error(err))
|
||||
h.response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
h.response.Success(c, nil, "删除成功")
|
||||
}
|
||||
|
||||
// PurchaseQuota 为下属购买额度
|
||||
func (h *SubordinateHandler) PurchaseQuota(c *gin.Context) {
|
||||
parentID := c.GetString("user_id")
|
||||
if parentID == "" {
|
||||
h.response.Unauthorized(c, "未登录")
|
||||
return
|
||||
}
|
||||
var cmd commands.PurchaseChildQuotaCommand
|
||||
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
cmd.ParentUserID = parentID
|
||||
if err := h.app.PurchaseChildQuota(c.Request.Context(), &cmd); err != nil {
|
||||
h.logger.Error("为下属购买额度失败", zap.Error(err))
|
||||
h.response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
h.response.Success(c, nil, "购买额度成功")
|
||||
}
|
||||
|
||||
// ListQuotaPurchases 下属额度购买记录
|
||||
func (h *SubordinateHandler) ListQuotaPurchases(c *gin.Context) {
|
||||
parentID := c.GetString("user_id")
|
||||
if parentID == "" {
|
||||
h.response.Unauthorized(c, "未登录")
|
||||
return
|
||||
}
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
cmd := &commands.ListChildQuotaPurchasesCommand{
|
||||
ParentUserID: parentID,
|
||||
ChildUserID: c.Query("child_user_id"),
|
||||
Page: page,
|
||||
PageSize: size,
|
||||
}
|
||||
if cmd.ChildUserID == "" {
|
||||
h.response.BadRequest(c, "child_user_id 不能为空")
|
||||
return
|
||||
}
|
||||
res, err := h.app.ListChildQuotaPurchases(c.Request.Context(), cmd)
|
||||
if err != nil {
|
||||
h.logger.Error("获取额度购买记录失败", zap.Error(err))
|
||||
h.response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
h.response.Success(c, res, "获取成功")
|
||||
}
|
||||
|
||||
// ListChildQuotaAccounts 下属额度账户
|
||||
func (h *SubordinateHandler) ListChildQuotaAccounts(c *gin.Context) {
|
||||
parentID := c.GetString("user_id")
|
||||
if parentID == "" {
|
||||
h.response.Unauthorized(c, "未登录")
|
||||
return
|
||||
}
|
||||
childID := c.Query("child_user_id")
|
||||
if childID == "" {
|
||||
h.response.BadRequest(c, "child_user_id 不能为空")
|
||||
return
|
||||
}
|
||||
res, err := h.app.ListChildQuotaAccounts(c.Request.Context(), &commands.ListChildQuotaAccountsCommand{
|
||||
ParentUserID: parentID,
|
||||
ChildUserID: childID,
|
||||
})
|
||||
if err != nil {
|
||||
h.logger.Error("获取下属额度账户失败", zap.Error(err))
|
||||
h.response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
h.response.Success(c, res, "获取成功")
|
||||
}
|
||||
|
||||
// ListMyQuotaAccounts 当前登录用户额度账户
|
||||
func (h *SubordinateHandler) ListMyQuotaAccounts(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
h.response.Unauthorized(c, "未登录")
|
||||
return
|
||||
}
|
||||
res, err := h.app.ListMyQuotaAccounts(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
h.logger.Error("获取我的额度账户失败", zap.Error(err))
|
||||
h.response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
h.response.Success(c, res, "获取成功")
|
||||
}
|
||||
@@ -49,8 +49,6 @@ func (r *CertificationRoutes) Register(router *http.GinRouter) {
|
||||
authGroup := certificationGroup.Group("")
|
||||
authGroup.Use(r.auth.Handle())
|
||||
{
|
||||
authGroup.GET("", r.handler.ListCertifications) // 查询认证列表(管理员)
|
||||
|
||||
// 1. 获取认证详情
|
||||
authGroup.GET("/details", r.handler.GetCertification)
|
||||
|
||||
@@ -71,10 +69,6 @@ func (r *CertificationRoutes) Register(router *http.GinRouter) {
|
||||
|
||||
// 前端确认是否完成签署
|
||||
authGroup.POST("/confirm-sign", r.handler.ConfirmSign)
|
||||
|
||||
// 管理员代用户完成认证(暂不关联合同)
|
||||
authGroup.POST("/admin/complete-without-contract", r.handler.AdminCompleteCertificationWithoutContract)
|
||||
|
||||
}
|
||||
|
||||
// 管理端企业审核(需管理员权限,以状态机状态为准)
|
||||
@@ -82,6 +76,8 @@ func (r *CertificationRoutes) Register(router *http.GinRouter) {
|
||||
adminGroup.Use(r.auth.Handle())
|
||||
adminGroup.Use(r.admin.Handle())
|
||||
{
|
||||
adminGroup.GET("", r.handler.ListCertifications) // 查询认证列表(管理员)
|
||||
adminGroup.POST("/complete-without-contract", r.handler.AdminCompleteCertificationWithoutContract)
|
||||
adminGroup.POST("/transition-status", r.handler.AdminTransitionCertificationStatus)
|
||||
}
|
||||
adminCertGroup := adminGroup.Group("/submit-records")
|
||||
|
||||
50
internal/infrastructure/http/routes/subordinate_routes.go
Normal file
50
internal/infrastructure/http/routes/subordinate_routes.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"tyapi-server/internal/infrastructure/http/handlers"
|
||||
sharedhttp "tyapi-server/internal/shared/http"
|
||||
"tyapi-server/internal/shared/middleware"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// SubordinateRoutes 下属与邀请路由
|
||||
type SubordinateRoutes struct {
|
||||
handler *handlers.SubordinateHandler
|
||||
auth *middleware.JWTAuthMiddleware
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewSubordinateRoutes 构造
|
||||
func NewSubordinateRoutes(
|
||||
handler *handlers.SubordinateHandler,
|
||||
auth *middleware.JWTAuthMiddleware,
|
||||
logger *zap.Logger,
|
||||
) *SubordinateRoutes {
|
||||
return &SubordinateRoutes{handler: handler, auth: auth, logger: logger}
|
||||
}
|
||||
|
||||
// Register 注册
|
||||
func (r *SubordinateRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
g := router.GetEngine()
|
||||
|
||||
g.POST("/api/v1/sub-portal/register", r.handler.SubPortalRegister)
|
||||
|
||||
sub := g.Group("/api/v1/subordinate")
|
||||
sub.Use(r.auth.Handle())
|
||||
{
|
||||
sub.POST("/invitations", r.handler.CreateInvitation)
|
||||
sub.GET("/subordinates", r.handler.ListSubordinates)
|
||||
sub.POST("/allocate", r.handler.Allocate)
|
||||
sub.GET("/allocations", r.handler.ListAllocations)
|
||||
sub.POST("/assign-subscription", r.handler.AssignSubscription)
|
||||
sub.GET("/child-subscriptions", r.handler.ListChildSubscriptions)
|
||||
sub.DELETE("/child-subscriptions/:subscription_id", r.handler.RemoveChildSubscription)
|
||||
sub.POST("/purchase-quota", r.handler.PurchaseQuota)
|
||||
sub.GET("/quota-purchases", r.handler.ListQuotaPurchases)
|
||||
sub.GET("/child-quotas", r.handler.ListChildQuotaAccounts)
|
||||
sub.GET("/my-quotas", r.handler.ListMyQuotaAccounts)
|
||||
}
|
||||
|
||||
r.logger.Info("下属账号路由注册完成")
|
||||
}
|
||||
31
internal/infrastructure/subordinate/account_kind_provider.go
Normal file
31
internal/infrastructure/subordinate/account_kind_provider.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package subordinate
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tyapi-server/internal/domains/subordinate/repositories"
|
||||
"tyapi-server/internal/shared/auth"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// AccountKindProviderImpl 从主从表判断 account_kind
|
||||
type AccountKindProviderImpl struct {
|
||||
repo repositories.SubordinateRepository
|
||||
}
|
||||
|
||||
// NewAccountKindProviderImpl 构造
|
||||
func NewAccountKindProviderImpl(repo repositories.SubordinateRepository) interfaces.AccountKindProvider {
|
||||
return &AccountKindProviderImpl{repo: repo}
|
||||
}
|
||||
|
||||
// AccountKind 返回 standalone 或 subordinate
|
||||
func (p *AccountKindProviderImpl) AccountKind(ctx context.Context, userID string) (string, error) {
|
||||
ok, err := p.repo.IsUserSubordinate(ctx, userID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if ok {
|
||||
return auth.AccountKindSubordinate, nil
|
||||
}
|
||||
return auth.AccountKindStandalone, nil
|
||||
}
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
|
||||
api_commands "tyapi-server/internal/application/api/commands"
|
||||
"tyapi-server/internal/application/api"
|
||||
finance_services "tyapi-server/internal/domains/finance/services"
|
||||
product_services "tyapi-server/internal/domains/product/services"
|
||||
@@ -84,25 +84,23 @@ func (h *ApiTaskHandler) HandleDeduction(ctx context.Context, t *asynq.Task) err
|
||||
zap.String("amount", payload.Amount),
|
||||
zap.String("transaction_id", payload.TransactionID))
|
||||
|
||||
// 调用钱包服务进行扣款
|
||||
if h.walletService != nil {
|
||||
amount, err := decimal.NewFromString(payload.Amount)
|
||||
if err != nil {
|
||||
h.logger.Error("金额格式错误", zap.Error(err))
|
||||
h.updateTaskStatus(ctx, t, "failed", "金额格式错误")
|
||||
return err
|
||||
}
|
||||
|
||||
if err := h.walletService.Deduct(ctx, payload.UserID, amount, payload.ApiCallID, payload.TransactionID, payload.ProductID); err != nil {
|
||||
h.logger.Error("扣款处理失败", zap.Error(err))
|
||||
h.updateTaskStatus(ctx, t, "failed", "扣款处理失败: "+err.Error())
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
h.logger.Warn("钱包服务未初始化,跳过扣款", zap.String("user_id", payload.UserID))
|
||||
h.updateTaskStatus(ctx, t, "failed", "钱包服务未初始化")
|
||||
// 统一走应用服务扣费链路(额度优先,钱包兜底)
|
||||
if h.apiApplicationService == nil {
|
||||
h.logger.Warn("API应用服务未初始化,无法处理扣款", zap.String("user_id", payload.UserID))
|
||||
h.updateTaskStatus(ctx, t, "failed", "API应用服务未初始化")
|
||||
return nil
|
||||
}
|
||||
if err := h.apiApplicationService.ProcessDeduction(ctx, &api_commands.ProcessDeductionCommand{
|
||||
UserID: payload.UserID,
|
||||
Amount: payload.Amount,
|
||||
ApiCallID: payload.ApiCallID,
|
||||
TransactionID: payload.TransactionID,
|
||||
ProductID: payload.ProductID,
|
||||
}); err != nil {
|
||||
h.logger.Error("扣款处理失败", zap.Error(err))
|
||||
h.updateTaskStatus(ctx, t, "failed", "扣款处理失败: "+err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新任务状态为成功
|
||||
h.updateTaskStatus(ctx, t, "completed", "")
|
||||
|
||||
7
internal/shared/auth/account_kind.go
Normal file
7
internal/shared/auth/account_kind.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package auth
|
||||
|
||||
// 账号在控制台维度的「壳」类型(与 user_type 管理员/普通 正交)
|
||||
const (
|
||||
AccountKindStandalone = "standalone"
|
||||
AccountKindSubordinate = "subordinate"
|
||||
)
|
||||
9
internal/shared/interfaces/account_kind_provider.go
Normal file
9
internal/shared/interfaces/account_kind_provider.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package interfaces
|
||||
|
||||
import "context"
|
||||
|
||||
// AccountKindProvider 为 JWT / 用户资料 提供 account_kind
|
||||
type AccountKindProvider interface {
|
||||
// AccountKind 返回 shared/auth 中的 AccountKind* 常量
|
||||
AccountKind(ctx context.Context, userID string) (string, error)
|
||||
}
|
||||
@@ -81,6 +81,11 @@ func (m *JWTAuthMiddleware) Handle() gin.HandlerFunc {
|
||||
c.Set("email", claims.Email)
|
||||
c.Set("phone", claims.Phone)
|
||||
c.Set("user_type", claims.UserType)
|
||||
if claims.AccountKind != "" {
|
||||
c.Set("account_kind", claims.AccountKind)
|
||||
} else {
|
||||
c.Set("account_kind", "standalone")
|
||||
}
|
||||
c.Set("token_claims", claims)
|
||||
|
||||
c.Next()
|
||||
@@ -99,6 +104,8 @@ type JWTClaims struct {
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
UserType string `json:"user_type"` // 新增:用户类型
|
||||
// AccountKind 控制台壳类型:standalone / subordinate(与主从关系表一致时下属为 subordinate)
|
||||
AccountKind string `json:"account_kind"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
@@ -137,15 +144,19 @@ func (m *JWTAuthMiddleware) respondUnauthorized(c *gin.Context, message string)
|
||||
}
|
||||
|
||||
// GenerateToken 生成JWT token
|
||||
func (m *JWTAuthMiddleware) GenerateToken(userID, phone, email, userType string) (string, error) {
|
||||
func (m *JWTAuthMiddleware) GenerateToken(userID, phone, email, userType, accountKind string) (string, error) {
|
||||
now := time.Now()
|
||||
if accountKind == "" {
|
||||
accountKind = "standalone"
|
||||
}
|
||||
|
||||
claims := &JWTClaims{
|
||||
UserID: userID,
|
||||
Username: phone, // 普通用户用手机号,管理员用用户名
|
||||
Email: email,
|
||||
Phone: phone,
|
||||
UserType: userType, // 新增:用户类型
|
||||
UserType: userType, // 新增:用户类型
|
||||
AccountKind: accountKind, // 下属 / 普通
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "tyapi-server",
|
||||
Subject: userID,
|
||||
@@ -262,6 +273,11 @@ func (m *OptionalAuthMiddleware) Handle() gin.HandlerFunc {
|
||||
c.Set("email", claims.Email)
|
||||
c.Set("phone", claims.Phone)
|
||||
c.Set("user_type", claims.UserType)
|
||||
if claims.AccountKind != "" {
|
||||
c.Set("account_kind", claims.AccountKind)
|
||||
} else {
|
||||
c.Set("account_kind", "standalone")
|
||||
}
|
||||
c.Set("token_claims", claims)
|
||||
|
||||
c.Next()
|
||||
@@ -343,6 +359,11 @@ func (m *AdminAuthMiddleware) Handle() gin.HandlerFunc {
|
||||
c.Set("email", claims.Email)
|
||||
c.Set("phone", claims.Phone)
|
||||
c.Set("user_type", claims.UserType)
|
||||
if claims.AccountKind != "" {
|
||||
c.Set("account_kind", claims.AccountKind)
|
||||
} else {
|
||||
c.Set("account_kind", "standalone")
|
||||
}
|
||||
c.Set("token_claims", claims)
|
||||
|
||||
c.Next()
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -403,9 +404,24 @@ func (m *DailyRateLimitMiddleware) checkReferer(c *gin.Context) error {
|
||||
|
||||
// 检查允许的Referer
|
||||
if len(m.limitConfig.AllowedReferers) > 0 {
|
||||
parsedReferer, err := url.Parse(referer)
|
||||
if err != nil || parsedReferer.Scheme == "" || parsedReferer.Host == "" {
|
||||
return fmt.Errorf("Referer格式无效")
|
||||
}
|
||||
refererOrigin := parsedReferer.Scheme + "://" + parsedReferer.Host
|
||||
|
||||
allowed := false
|
||||
for _, allowedRef := range m.limitConfig.AllowedReferers {
|
||||
if strings.Contains(referer, allowedRef) {
|
||||
allowedRef = strings.TrimSpace(allowedRef)
|
||||
if allowedRef == "" {
|
||||
continue
|
||||
}
|
||||
parsedAllowed, parseErr := url.Parse(allowedRef)
|
||||
if parseErr != nil || parsedAllowed.Scheme == "" || parsedAllowed.Host == "" {
|
||||
continue
|
||||
}
|
||||
allowedOrigin := parsedAllowed.Scheme + "://" + parsedAllowed.Host
|
||||
if refererOrigin == allowedOrigin {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user