This commit is contained in:
33
README.md
33
README.md
@@ -489,17 +489,22 @@ junhong_cmp_fiber/
|
||||
└────────────┬────────────┘
|
||||
│
|
||||
┌────────────▼────────────┐
|
||||
│ 4. 认证中间件 │
|
||||
│ 4. CORS 中间件 │
|
||||
│ (跨域预检) │
|
||||
└────────────┬────────────┘
|
||||
│
|
||||
┌────────────▼────────────┐
|
||||
│ 5. 认证中间件 │
|
||||
│ (按路由组配置) │ ─── 模块化路由注册
|
||||
└────────────┬────────────┘
|
||||
│
|
||||
┌────────────▼────────────┐
|
||||
│ 5. RateLimiter 中间件 │
|
||||
│ 6. RateLimiter 中间件 │
|
||||
│ (限流) │ ─── 可选 (config: enable_rate_limiter)
|
||||
└────────────┬────────────┘
|
||||
│
|
||||
┌────────────▼────────────┐
|
||||
│ 6. 路由处理器 │
|
||||
│ 7. 路由处理器 │
|
||||
│ (业务逻辑) │
|
||||
└────────────┬────────────┘
|
||||
│
|
||||
@@ -538,12 +543,22 @@ junhong_cmp_fiber/
|
||||
- **始终激活**:是
|
||||
- **日志格式**:包含字段的 JSON:timestamp、level、method、path、status、duration_ms、request_id、ip、user_agent、user_id
|
||||
|
||||
#### 4. 认证中间件(pkg/middleware/auth.go 和 internal/middleware/)
|
||||
#### 4. CORS 中间件(Fiber cors)
|
||||
- **用途**:允许已配置前端域名跨域访问 API
|
||||
- **行为**:
|
||||
- 对浏览器预检 `OPTIONS` 请求返回 204
|
||||
- 添加 `Access-Control-Allow-Origin`、`Access-Control-Allow-Methods`、`Access-Control-Allow-Headers`
|
||||
- 必须在业务路由和认证中间件之前注册,避免预检请求被 401 拦截
|
||||
- **配置**:`middleware.cors.*`(默认启用)
|
||||
- **默认来源**:`*`(全部来源)。使用通配来源时 `allow_credentials` 必须为 `false`
|
||||
|
||||
#### 5. 认证中间件(pkg/middleware/auth.go 和 internal/middleware/)
|
||||
- **用途**:使用 Token 验证对请求进行认证
|
||||
- **行为**:
|
||||
- 从 `Authorization: Bearer {token}` 请求头提取 token
|
||||
- 通过 TokenValidator 函数验证 token(支持 JWT 和 Redis Token)
|
||||
- 如果缺失/无效 token 返回 401
|
||||
- `OPTIONS` 预检请求直接返回 204,不做 Token 校验
|
||||
- 成功时将用户信息存储在上下文中(UserID、UserType、ShopID、EnterpriseID)
|
||||
- **实现方式**:模块化路由注册(无全局配置)
|
||||
- `/api/admin/*`:后台认证(SuperAdmin、Platform、Agent)
|
||||
@@ -555,7 +570,7 @@ junhong_cmp_fiber/
|
||||
- 1002:无效或过期 token
|
||||
- 1003:权限不足
|
||||
|
||||
#### 5. RateLimiter 中间件(internal/middleware/ratelimit.go)
|
||||
#### 6. RateLimiter 中间件(internal/middleware/ratelimit.go)
|
||||
- **用途**:通过限制请求速率保护 API 免受滥用
|
||||
- **行为**:
|
||||
- 按客户端 IP 地址追踪请求
|
||||
@@ -568,7 +583,7 @@ junhong_cmp_fiber/
|
||||
- `redis`:基于 Redis(分布式,持久化)
|
||||
- **错误码**:1003(请求过于频繁)
|
||||
|
||||
#### 6. 路由处理器
|
||||
#### 7. 路由处理器
|
||||
- **用途**:执行端点的业务逻辑
|
||||
- **可用上下文数据**:
|
||||
- 请求 ID:`c.Locals(constants.ContextKeyRequestID)`
|
||||
@@ -582,6 +597,12 @@ junhong_cmp_fiber/
|
||||
app.Use(recover.New())
|
||||
app.Use(addRequestID())
|
||||
app.Use(loggerMiddleware())
|
||||
app.Use(cors.New(cors.Config{
|
||||
AllowOrigins: config.GetConfig().Middleware.CORS.AllowOrigins,
|
||||
AllowMethods: config.GetConfig().Middleware.CORS.AllowMethods,
|
||||
AllowHeaders: config.GetConfig().Middleware.CORS.AllowHeaders,
|
||||
AllowCredentials: config.GetConfig().Middleware.CORS.AllowCredentials,
|
||||
}))
|
||||
|
||||
// 模块化路由注册(认证中间件按路由组配置)
|
||||
routes.RegisterRoutes(app, handlers, middlewares)
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/compress"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/requestid"
|
||||
"github.com/google/uuid"
|
||||
"github.com/redis/go-redis/v9"
|
||||
@@ -288,6 +289,23 @@ func initMiddleware(app *fiber.App, cfg *config.Config, appLogger *zap.Logger) {
|
||||
|
||||
// 4. Logger - 记录所有请求(在 Compress 之后注册,读到的是未压缩的原始响应体)
|
||||
app.Use(logger.Middleware())
|
||||
|
||||
// 5. CORS - 必须在业务路由和认证中间件之前注册,确保预检请求不被认证拦截
|
||||
if cfg.Middleware.CORS.Enabled {
|
||||
app.Use(cors.New(cors.Config{
|
||||
AllowOrigins: cfg.Middleware.CORS.AllowOrigins,
|
||||
AllowMethods: cfg.Middleware.CORS.AllowMethods,
|
||||
AllowHeaders: cfg.Middleware.CORS.AllowHeaders,
|
||||
ExposeHeaders: cfg.Middleware.CORS.ExposeHeaders,
|
||||
AllowCredentials: cfg.Middleware.CORS.AllowCredentials,
|
||||
MaxAge: cfg.Middleware.CORS.MaxAge,
|
||||
}))
|
||||
appLogger.Info("CORS 中间件已启用",
|
||||
zap.String("allow_origins", cfg.Middleware.CORS.AllowOrigins),
|
||||
zap.String("allow_methods", cfg.Middleware.CORS.AllowMethods),
|
||||
zap.Bool("allow_credentials", cfg.Middleware.CORS.AllowCredentials),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// initRoutes 注册路由
|
||||
|
||||
@@ -56,6 +56,10 @@ services:
|
||||
# 日志配置
|
||||
- JUNHONG_LOGGING_LEVEL=info
|
||||
- JUNHONG_LOGGING_DEVELOPMENT=false
|
||||
# 跨域配置
|
||||
- JUNHONG_MIDDLEWARE_CORS_ENABLED=true
|
||||
- JUNHONG_MIDDLEWARE_CORS_ALLOW_ORIGINS=*
|
||||
- JUNHONG_MIDDLEWARE_CORS_ALLOW_CREDENTIALS=false
|
||||
# 对象存储配置
|
||||
- JUNHONG_STORAGE_PROVIDER=s3
|
||||
- JUNHONG_STORAGE_S3_ENDPOINT=https://obs-helf.cucloud.cn
|
||||
|
||||
@@ -32,6 +32,10 @@ func NewPersonalAuthMiddleware(jwtManager *auth.JWTManager, rdb *redis.Client, l
|
||||
// JWT + Redis 双重校验:先验证 JWT 签名和有效期,再检查 Redis 中 token 是否存在
|
||||
func (m *PersonalAuthMiddleware) Authenticate() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
if c.Method() == fiber.MethodOptions {
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
authHeader := c.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
m.logger.Warn("个人客户认证失败:缺少 Authorization header",
|
||||
|
||||
@@ -96,6 +96,7 @@ type LogRotationConfig struct {
|
||||
type MiddlewareConfig struct {
|
||||
EnableRateLimiter bool `mapstructure:"enable_rate_limiter"` // 启用限流器(默认:false)
|
||||
RateLimiter RateLimiterConfig `mapstructure:"rate_limiter"` // 限流器配置
|
||||
CORS CORSConfig `mapstructure:"cors"` // 跨域配置
|
||||
}
|
||||
|
||||
// ClientConfig 客户端行为开关配置
|
||||
@@ -110,6 +111,17 @@ type RateLimiterConfig struct {
|
||||
Storage string `mapstructure:"storage"` // "memory" 或 "redis"
|
||||
}
|
||||
|
||||
// CORSConfig 跨域资源共享配置
|
||||
type CORSConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"` // 是否启用 CORS
|
||||
AllowOrigins string `mapstructure:"allow_origins"` // 允许的来源,多个来源用逗号分隔
|
||||
AllowMethods string `mapstructure:"allow_methods"` // 允许的 HTTP 方法,多个方法用逗号分隔
|
||||
AllowHeaders string `mapstructure:"allow_headers"` // 允许的请求头,多个请求头用逗号分隔
|
||||
ExposeHeaders string `mapstructure:"expose_headers"` // 暴露给浏览器读取的响应头,多个响应头用逗号分隔
|
||||
AllowCredentials bool `mapstructure:"allow_credentials"` // 是否允许携带凭证
|
||||
MaxAge int `mapstructure:"max_age"` // 预检结果缓存秒数
|
||||
}
|
||||
|
||||
// SMSConfig 短信服务配置
|
||||
type SMSConfig struct {
|
||||
GatewayURL string `mapstructure:"gateway_url"` // 短信网关地址
|
||||
@@ -282,6 +294,20 @@ func (c *Config) Validate() error {
|
||||
if c.Middleware.RateLimiter.Storage != "" && !validStorage[c.Middleware.RateLimiter.Storage] {
|
||||
return fmt.Errorf("invalid configuration: middleware.rate_limiter.storage: invalid storage type (current value: %s, expected: memory or redis)", c.Middleware.RateLimiter.Storage)
|
||||
}
|
||||
if c.Middleware.CORS.Enabled {
|
||||
if strings.TrimSpace(c.Middleware.CORS.AllowOrigins) == "" {
|
||||
return fmt.Errorf("invalid configuration: middleware.cors.allow_origins: must be non-empty when CORS is enabled")
|
||||
}
|
||||
if c.Middleware.CORS.AllowCredentials && strings.TrimSpace(c.Middleware.CORS.AllowOrigins) == "*" {
|
||||
return fmt.Errorf("invalid configuration: middleware.cors.allow_origins: wildcard origin is not allowed when allow_credentials is true")
|
||||
}
|
||||
if strings.TrimSpace(c.Middleware.CORS.AllowMethods) == "" {
|
||||
return fmt.Errorf("invalid configuration: middleware.cors.allow_methods: must be non-empty when CORS is enabled")
|
||||
}
|
||||
if c.Middleware.CORS.MaxAge < 0 {
|
||||
return fmt.Errorf("invalid configuration: middleware.cors.max_age: must be greater than or equal to 0")
|
||||
}
|
||||
}
|
||||
|
||||
// 短信服务验证(可选,配置 GatewayURL 时才验证其他字段)
|
||||
if c.SMS.GatewayURL != "" {
|
||||
|
||||
@@ -90,6 +90,14 @@ middleware:
|
||||
max: 100
|
||||
expiration: "1m"
|
||||
storage: "memory"
|
||||
cors:
|
||||
enabled: true
|
||||
allow_origins: "*"
|
||||
allow_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS"
|
||||
allow_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With,X-Request-ID"
|
||||
expose_headers: "Content-Length,X-Request-ID"
|
||||
allow_credentials: false
|
||||
max_age: 86400
|
||||
|
||||
# 客户端配置
|
||||
client:
|
||||
|
||||
@@ -106,6 +106,13 @@ func bindEnvVariables(v *viper.Viper) {
|
||||
"middleware.rate_limiter.max",
|
||||
"middleware.rate_limiter.expiration",
|
||||
"middleware.rate_limiter.storage",
|
||||
"middleware.cors.enabled",
|
||||
"middleware.cors.allow_origins",
|
||||
"middleware.cors.allow_methods",
|
||||
"middleware.cors.allow_headers",
|
||||
"middleware.cors.expose_headers",
|
||||
"middleware.cors.allow_credentials",
|
||||
"middleware.cors.max_age",
|
||||
"sms.gateway_url",
|
||||
"sms.username",
|
||||
"sms.password",
|
||||
@@ -118,6 +125,9 @@ func bindEnvVariables(v *viper.Viper) {
|
||||
"gateway.app_id",
|
||||
"gateway.app_secret",
|
||||
"gateway.timeout",
|
||||
"polling.verbose_log",
|
||||
"polling_auto_trigger.enable_auto_trigger",
|
||||
"polling_auto_trigger.auto_trigger_system_user_id",
|
||||
"worker.role",
|
||||
"worker.instance_name",
|
||||
"wechat.official_account.app_id",
|
||||
|
||||
@@ -223,6 +223,10 @@ type AuthConfig struct {
|
||||
// 所有错误统一返回 AppError,由全局 ErrorHandler 处理
|
||||
func Auth(config AuthConfig) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
if c.Method() == fiber.MethodOptions {
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
// 检查是否跳过认证
|
||||
path := c.Path()
|
||||
for _, skipPath := range config.SkipPaths {
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 运行所有代码规范检查
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 运行代码规范检查..."
|
||||
echo ""
|
||||
|
||||
bash scripts/check-service-errors.sh
|
||||
bash scripts/check-comment-paths.sh
|
||||
|
||||
echo ""
|
||||
echo "✅ 所有检查通过"
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 检查注释中的 API 路径是否一致
|
||||
|
||||
echo "🔍 检查注释中的 API 路径..."
|
||||
|
||||
VIOLATIONS=$(grep -rn "/api/v1" internal/handler/ 2>/dev/null | grep -v "_test.go")
|
||||
|
||||
if [ -n "$VIOLATIONS" ]; then
|
||||
echo ""
|
||||
echo "❌ 发现残留的 /api/v1 路径注释:"
|
||||
echo "$VIOLATIONS"
|
||||
echo ""
|
||||
echo "请修复为真实路径(/api/admin、/api/h5、/api/c/v1)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ 注释路径检查通过"
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 检查 Service 层是否使用 fmt.Errorf 对外返回
|
||||
|
||||
echo "🔍 检查 Service 层错误处理规范..."
|
||||
|
||||
FILES=$(find internal/service -name "*.go" -type f 2>/dev/null)
|
||||
if [ -z "$FILES" ]; then
|
||||
echo "⚠️ 未找到 Service 层文件"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
VIOLATIONS=$(grep -n "fmt\.Errorf" $FILES | grep -v "// whitelist:")
|
||||
|
||||
if [ -n "$VIOLATIONS" ]; then
|
||||
echo ""
|
||||
echo "❌ 发现 Service 层使用 fmt.Errorf:"
|
||||
echo "$VIOLATIONS"
|
||||
echo ""
|
||||
echo "请使用以下方式替代:"
|
||||
echo " - 业务错误:errors.New(code, msg)"
|
||||
echo " - 系统错误:errors.Wrap(code, err, msg)"
|
||||
echo ""
|
||||
echo "如果某处确实需要使用 fmt.Errorf(如内部调试),请添加注释:// whitelist:"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Service 层错误处理检查通过"
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Fix role_assignment_limit_test.go - add shopRoleStore declarations
|
||||
cd /Users/break/csxjProject/junhong_cmp_fiber
|
||||
|
||||
# Add shopRoleStore for all test functions
|
||||
sed -i.bak3 '74a\
|
||||
shopRoleStore := postgres.NewShopRoleStore(tx, rdb)
|
||||
' tests/unit/role_assignment_limit_test.go
|
||||
|
||||
sed -i.bak4 '122a\
|
||||
shopRoleStore := postgres.NewShopRoleStore(tx, rdb)
|
||||
' tests/unit/role_assignment_limit_test.go
|
||||
|
||||
sed -i.bak5 '170a\
|
||||
shopRoleStore := postgres.NewShopRoleStore(tx, rdb)
|
||||
' tests/unit/role_assignment_limit_test.go
|
||||
|
||||
# Fix shop_service_test.go - add shopRoleStore and roleStore
|
||||
sed -i.bak6 '26a\
|
||||
shopRoleStore := postgres.NewShopRoleStore(tx, rdb)\
|
||||
roleStore := postgres.NewRoleStore(tx)
|
||||
' tests/unit/shop_service_test.go
|
||||
|
||||
echo "All test files fixed!"
|
||||
@@ -1,78 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import re
|
||||
|
||||
# 1. Fix internal/service/account/service_test.go
|
||||
print("Fixing service_test.go...")
|
||||
with open('internal/service/account/service_test.go', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
content = re.sub(
|
||||
r'(\taccountRoleStore := postgres\.NewAccountRoleStore\(tx, rdb\)\n)',
|
||||
r'\1\tshopRoleStore := postgres.NewShopRoleStore(tx, rdb)\n',
|
||||
content,
|
||||
count=1
|
||||
)
|
||||
|
||||
content = re.sub(
|
||||
r'New\(accountStore, roleStore, accountRoleStore, (&MockShopStore{[^}]*}), (&MockEnterpriseStore{[^}]*}), (&MockAuditService{[^}]*})\)',
|
||||
r'New(accountStore, roleStore, accountRoleStore, nil, \1, \2, \3)',
|
||||
content
|
||||
)
|
||||
|
||||
with open('internal/service/account/service_test.go', 'w') as f:
|
||||
f.write(content)
|
||||
print("✓ Fixed service_test.go")
|
||||
|
||||
# 2. Fix tests/unit/permission_check_test.go
|
||||
print("Fixing permission_check_test.go...")
|
||||
with open('tests/unit/permission_check_test.go', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
content = re.sub(
|
||||
r'permission\.New\(permissionStore, accountRoleStore, rolePermStore, rdb\)',
|
||||
'permission.New(permissionStore, accountRoleStore, rolePermStore, nil, rdb)',
|
||||
content
|
||||
)
|
||||
|
||||
with open('tests/unit/permission_check_test.go', 'w') as f:
|
||||
f.write(content)
|
||||
print("✓ Fixed permission_check_test.go")
|
||||
|
||||
# 3. Fix tests/unit/permission_cache_test.go
|
||||
print("Fixing permission_cache_test.go...")
|
||||
with open('tests/unit/permission_cache_test.go', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
content = re.sub(
|
||||
r'permission\.New\(permissionStore, accountRoleStore, rolePermStore, rdb\)',
|
||||
'permission.New(permissionStore, accountRoleStore, rolePermStore, nil, rdb)',
|
||||
content
|
||||
)
|
||||
|
||||
with open('tests/unit/permission_cache_test.go', 'w') as f:
|
||||
f.write(content)
|
||||
print("✓ Fixed permission_cache_test.go")
|
||||
|
||||
# 4. Fix tests/unit/shop_service_test.go
|
||||
print("Fixing shop_service_test.go...")
|
||||
with open('tests/unit/shop_service_test.go', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
content = re.sub(
|
||||
r'(\taccountStore := postgres\.NewAccountStore\(tx, rdb\)\n)',
|
||||
r'\1\tshopRoleStore := postgres.NewShopRoleStore(tx, rdb)\n\troleStore := postgres.NewRoleStore(tx)\n',
|
||||
content,
|
||||
count=1
|
||||
)
|
||||
|
||||
content = re.sub(
|
||||
r'shop\.New\(shopStore, accountStore\)',
|
||||
'shop.New(shopStore, accountStore, shopRoleStore, roleStore)',
|
||||
content
|
||||
)
|
||||
|
||||
with open('tests/unit/shop_service_test.go', 'w') as f:
|
||||
f.write(content)
|
||||
print("✓ Fixed shop_service_test.go")
|
||||
|
||||
print("\nAll files fixed!")
|
||||
@@ -1,92 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""修复测试文件中的构造函数调用"""
|
||||
|
||||
import re
|
||||
|
||||
# 修复 account_role_test.go
|
||||
with open('tests/integration/account_role_test.go', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# 添加 shopRoleStore 初始化
|
||||
content = re.sub(
|
||||
r'(\tenpriseStore := postgresStore\.NewEnterpriseStore\(env\.TX, env\.Redis\)\n)',
|
||||
r'\1\tshopRoleStore := postgresStore.NewShopRoleStore(env.TX, env.Redis)\n',
|
||||
content
|
||||
)
|
||||
|
||||
# 修复 accountService.New 调用
|
||||
content = re.sub(
|
||||
r'accountService\.New\(accountStore, roleStore, accountRoleStore, shopStore, enterpriseStore, auditService\)',
|
||||
'accountService.New(accountStore, roleStore, accountRoleStore, shopRoleStore, shopStore, enterpriseStore, auditService)',
|
||||
content
|
||||
)
|
||||
|
||||
with open('tests/integration/account_role_test.go', 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
print("Fixed account_role_test.go")
|
||||
|
||||
# 修复 role_assignment_limit_test.go
|
||||
with open('tests/unit/role_assignment_limit_test.go', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# 添加 shopRoleStore 初始化
|
||||
content = re.sub(
|
||||
r'(\tenpriseStore := postgres\.NewEnterpriseStore\(tx, rdb\)\n)',
|
||||
r'\1\tshopRoleStore := postgres.NewShopRoleStore(tx, rdb)\n',
|
||||
content,
|
||||
count=1 # 只替换第一个
|
||||
)
|
||||
|
||||
# 修复 account.New 调用
|
||||
content = re.sub(
|
||||
r'account\.New\(accountStore, roleStore, accountRoleStore, shopStore, enterpriseStore, auditService\)',
|
||||
'account.New(accountStore, roleStore, accountRoleStore, shopRoleStore, shopStore, enterpriseStore, auditService)',
|
||||
content
|
||||
)
|
||||
|
||||
with open('tests/unit/role_assignment_limit_test.go', 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
print("Fixed role_assignment_limit_test.go")
|
||||
|
||||
# 修复 permission_platform_filter_test.go
|
||||
with open('tests/unit/permission_platform_filter_test.go', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# 修复 permission.New 调用 - 需要添加 nil 作为第4个参数 (accountService)
|
||||
content = re.sub(
|
||||
r'permission\.New\(permissionStore, accountRoleStore, rolePermStore, rdb\)',
|
||||
'permission.New(permissionStore, accountRoleStore, rolePermStore, nil, rdb)',
|
||||
content
|
||||
)
|
||||
|
||||
with open('tests/unit/permission_platform_filter_test.go', 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
print("Fixed permission_platform_filter_test.go")
|
||||
|
||||
# 修复 account_audit_test.go
|
||||
with open('tests/integration/account_audit_test.go', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# 添加 shopRoleStore 初始化
|
||||
content = re.sub(
|
||||
r'(\t\tenpriseStore := postgres\.NewEnterpriseStore\(env\.TX, env\.Redis\)\n)',
|
||||
r'\1\t\tshopRoleStore := postgres.NewShopRoleStore(env.TX, env.Redis)\n',
|
||||
content
|
||||
)
|
||||
|
||||
# 修复 accountSvc.New 调用
|
||||
content = re.sub(
|
||||
r'accountSvc\.New\(accountStore, roleStore, accountRoleStore, shopStore, enterpriseStore, auditService\)',
|
||||
'accountSvc.New(accountStore, roleStore, accountRoleStore, shopRoleStore, shopStore, enterpriseStore, auditService)',
|
||||
content
|
||||
)
|
||||
|
||||
with open('tests/integration/account_audit_test.go', 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
print("Fixed account_audit_test.go")
|
||||
|
||||
print("\n所有文件修复完成!")
|
||||
@@ -1,156 +0,0 @@
|
||||
-- 轮询系统初始化配置脚本
|
||||
-- 设计目标: 支持一亿张卡规模
|
||||
-- 执行: psql -U user -d database -f scripts/init_polling_config.sql
|
||||
|
||||
-- ========================================
|
||||
-- 1. 轮询配置初始化
|
||||
-- 设计原则:
|
||||
-- - 未实名卡:5分钟检查一次(避免过于频繁)
|
||||
-- - 已实名卡:每天检查一次(状态稳定)
|
||||
-- - 激活卡流量:每小时检查一次
|
||||
-- ========================================
|
||||
|
||||
-- 删除已有配置(如果存在)
|
||||
DELETE FROM tb_polling_config;
|
||||
|
||||
-- 优先级 10: 未实名卡(中频检查,每5分钟)
|
||||
-- 预估:1000万未实名卡 × 12次/小时 = 1.2亿次/小时
|
||||
INSERT INTO tb_polling_config (config_name, card_condition, card_category, carrier_id, priority, realname_check_interval, carddata_check_interval, package_check_interval, status, description)
|
||||
VALUES
|
||||
('未实名卡轮询', 'not_real_name', NULL, NULL, 10, 300, NULL, NULL, 1, '未实名卡每5分钟检查一次实名状态(一亿卡规模优化)');
|
||||
|
||||
-- 优先级 15: 行业卡(无需实名检查)
|
||||
INSERT INTO tb_polling_config (config_name, card_condition, card_category, carrier_id, priority, realname_check_interval, carddata_check_interval, package_check_interval, status, description)
|
||||
VALUES
|
||||
('行业卡轮询', NULL, 'industry', NULL, 15, NULL, 3600, 3600, 1, '行业卡无需实名检查,每小时检查流量和套餐');
|
||||
|
||||
-- 优先级 20: 已实名卡(低频检查,每天一次)
|
||||
-- 预估:3000万已实名卡 × 1次/天 = 很少
|
||||
INSERT INTO tb_polling_config (config_name, card_condition, card_category, carrier_id, priority, realname_check_interval, carddata_check_interval, package_check_interval, status, description)
|
||||
VALUES
|
||||
('已实名卡轮询', 'real_name', NULL, NULL, 20, 86400, NULL, NULL, 1, '已实名卡每天检查一次实名状态(状态稳定,无需频繁检查)');
|
||||
|
||||
-- 优先级 30: 已激活卡(流量和套餐检查,每小时)
|
||||
-- 预估:6000万激活卡 × 1次/小时 = 6000万次/小时
|
||||
INSERT INTO tb_polling_config (config_name, card_condition, card_category, carrier_id, priority, realname_check_interval, carddata_check_interval, package_check_interval, status, description)
|
||||
VALUES
|
||||
('已激活卡轮询', 'activated', NULL, NULL, 30, NULL, 3600, 3600, 1, '已激活卡每小时检查流量和套餐(一亿卡规模优化)');
|
||||
|
||||
-- 优先级 100: 默认配置(兜底,保守策略)
|
||||
INSERT INTO tb_polling_config (config_name, card_condition, card_category, carrier_id, priority, realname_check_interval, carddata_check_interval, package_check_interval, status, description)
|
||||
VALUES
|
||||
('默认轮询配置', NULL, NULL, NULL, 100, 86400, 86400, 86400, 1, '默认配置,每天检查一次(未匹配其他配置的卡)');
|
||||
|
||||
-- ========================================
|
||||
-- 2. 并发控制配置初始化
|
||||
-- 设计目标:支持 5 万 QPS 吞吐
|
||||
-- 单 Worker 建议:500-1000 并发
|
||||
-- 多 Worker 部署:8-16 个 Worker
|
||||
-- ========================================
|
||||
|
||||
-- 删除已有配置(如果存在)
|
||||
DELETE FROM tb_polling_concurrency_config;
|
||||
|
||||
-- 实名检查并发数(单 Worker)
|
||||
INSERT INTO tb_polling_concurrency_config (task_type, max_concurrency, description)
|
||||
VALUES
|
||||
('realname', 500, '实名检查任务最大并发数(单 Worker,可部署多个 Worker 水平扩展)');
|
||||
|
||||
-- 卡流量检查并发数(单 Worker)
|
||||
INSERT INTO tb_polling_concurrency_config (task_type, max_concurrency, description)
|
||||
VALUES
|
||||
('carddata', 1000, '流量检查任务最大并发数(单 Worker,流量检查占比最大)');
|
||||
|
||||
-- 套餐检查并发数(单 Worker)
|
||||
INSERT INTO tb_polling_concurrency_config (task_type, max_concurrency, description)
|
||||
VALUES
|
||||
('package', 500, '套餐检查任务最大并发数(单 Worker)');
|
||||
|
||||
-- 停复机操作并发数(单 Worker)
|
||||
INSERT INTO tb_polling_concurrency_config (task_type, max_concurrency, description)
|
||||
VALUES
|
||||
('stop_start', 100, '停复机操作最大并发数(需要谨慎控制)');
|
||||
|
||||
-- ========================================
|
||||
-- 3. 数据清理配置初始化
|
||||
-- 一亿卡规模每天产生大量数据,需要及时清理
|
||||
-- ========================================
|
||||
|
||||
-- 删除已有配置(如果存在)
|
||||
DELETE FROM tb_data_cleanup_config;
|
||||
|
||||
-- 流量历史记录清理配置(保留较短时间)
|
||||
INSERT INTO tb_data_cleanup_config (table_name, retention_days, enabled, batch_size, description)
|
||||
VALUES
|
||||
('tb_data_usage_record', 30, 1, 50000, '保留30天流量历史,每批删除5万条(一亿卡每天产生大量数据)');
|
||||
|
||||
-- 操作日志清理配置
|
||||
INSERT INTO tb_data_cleanup_config (table_name, retention_days, enabled, batch_size, description)
|
||||
VALUES
|
||||
('tb_account_operation_log', 90, 1, 50000, '保留90天操作日志,每批删除5万条');
|
||||
|
||||
-- 告警历史清理配置
|
||||
INSERT INTO tb_data_cleanup_config (table_name, retention_days, enabled, batch_size, description)
|
||||
VALUES
|
||||
('tb_polling_alert_history', 14, 1, 50000, '保留14天告警历史,每批删除5万条');
|
||||
|
||||
-- 手动触发日志清理配置
|
||||
INSERT INTO tb_data_cleanup_config (table_name, retention_days, enabled, batch_size, description)
|
||||
VALUES
|
||||
('tb_polling_manual_trigger_log', 30, 1, 50000, '保留30天手动触发日志,每批删除5万条');
|
||||
|
||||
-- 数据清理日志清理配置
|
||||
INSERT INTO tb_data_cleanup_config (table_name, retention_days, enabled, batch_size, description)
|
||||
VALUES
|
||||
('tb_data_cleanup_log', 60, 1, 10000, '保留60天数据清理日志');
|
||||
|
||||
-- ========================================
|
||||
-- 4. 告警规则初始化(一亿卡规模)
|
||||
-- ========================================
|
||||
|
||||
-- 删除已有规则(如果存在)
|
||||
DELETE FROM tb_polling_alert_rule;
|
||||
|
||||
-- 队列积压告警(阈值调高,适应大规模)
|
||||
INSERT INTO tb_polling_alert_rule (rule_name, task_type, metric_type, operator, threshold, alert_level, cooldown_minutes, status, notify_channels, description)
|
||||
VALUES
|
||||
('实名检查队列积压', 'polling:realname', 'queue_size', '>', 500000, 'warning', 10, 1, 'log', '实名检查队列超过50万时告警'),
|
||||
('流量检查队列积压', 'polling:carddata', 'queue_size', '>', 1000000, 'warning', 10, 1, 'log', '流量检查队列超过100万时告警'),
|
||||
('实名检查队列严重积压', 'polling:realname', 'queue_size', '>', 2000000, 'critical', 5, 1, 'log', '实名检查队列超过200万时严重告警');
|
||||
|
||||
-- 失败率告警
|
||||
INSERT INTO tb_polling_alert_rule (rule_name, task_type, metric_type, operator, threshold, alert_level, cooldown_minutes, status, notify_channels, description)
|
||||
VALUES
|
||||
('实名检查失败率过高', 'polling:realname', 'failure_rate', '>', 20, 'warning', 10, 1, 'log', '实名检查失败率超过20%时告警'),
|
||||
('流量检查失败率过高', 'polling:carddata', 'failure_rate', '>', 20, 'warning', 10, 1, 'log', '流量检查失败率超过20%时告警');
|
||||
|
||||
-- ========================================
|
||||
-- 初始化完成
|
||||
-- ========================================
|
||||
|
||||
-- 验证初始化结果
|
||||
SELECT '轮询配置初始化完成' AS message, COUNT(*) AS count FROM tb_polling_config;
|
||||
SELECT '并发控制配置初始化完成' AS message, COUNT(*) AS count FROM tb_polling_concurrency_config;
|
||||
SELECT '数据清理配置初始化完成' AS message, COUNT(*) AS count FROM tb_data_cleanup_config;
|
||||
SELECT '告警规则初始化完成' AS message, COUNT(*) AS count FROM tb_polling_alert_rule;
|
||||
|
||||
-- ========================================
|
||||
-- 容量规划参考(一亿卡)
|
||||
-- ========================================
|
||||
--
|
||||
-- 检查次数估算(按上述配置):
|
||||
-- - 未实名卡(10%):1000万 × 12次/小时 = 1.2亿次/小时
|
||||
-- - 已实名卡(30%):3000万 × 1次/天 ≈ 125万次/小时
|
||||
-- - 激活卡流量(60%):6000万 × 1次/小时 = 6000万次/小时
|
||||
-- - 激活卡套餐(60%):6000万 × 1次/小时 = 6000万次/小时
|
||||
-- 总计:约 2.4 亿次/小时 = 6.7万次/秒
|
||||
--
|
||||
-- 推荐部署:
|
||||
-- - Worker 数量:16 个(每个处理约 4000 QPS)
|
||||
-- - Redis 内存:16GB+(缓存 + 队列)
|
||||
-- - 数据库连接池:每 Worker 50 连接
|
||||
-- - Asynq 队列:critical/default/low 三个队列
|
||||
--
|
||||
-- 初始化时间估算:
|
||||
-- - 1000 万卡:约 50 秒(10万/批,500ms间隔)
|
||||
-- - 1 亿卡:约 500 秒 ≈ 8 分钟
|
||||
@@ -4,8 +4,9 @@
|
||||
# ============================================================================
|
||||
# 使用方法:
|
||||
# 1. 首次设置:./scripts/setup-env.sh
|
||||
# 2. 加载环境:source .env.local
|
||||
# 3. 启动服务:go run cmd/api/main.go
|
||||
# 2. 指定文件:./scripts/setup-env.sh .env.gray
|
||||
# 3. 加载环境:source .env.local
|
||||
# 4. 启动服务:go run cmd/api/main.go
|
||||
# ============================================================================
|
||||
|
||||
set -e
|
||||
@@ -18,8 +19,111 @@ BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 配置文件路径
|
||||
ENV_FILE=".env.local"
|
||||
# 配置文件路径,可通过 ENV_FILE=/tmp/demo.env ./scripts/setup-env.sh 覆盖,便于本地试生成。
|
||||
ENV_FILE="${ENV_FILE:-.env.local}"
|
||||
# 输出格式:shell 适合 source,systemd 适合 EnvironmentFile=。
|
||||
ENV_FORMAT="${ENV_FORMAT:-shell}"
|
||||
|
||||
show_usage() {
|
||||
cat <<'EOF'
|
||||
君鸿卡管系统 - 环境变量配置向导
|
||||
|
||||
用法:
|
||||
./scripts/setup-env.sh
|
||||
./scripts/setup-env.sh .env.gray
|
||||
./scripts/setup-env.sh --file .env.gray
|
||||
./scripts/setup-env.sh -f .env.gray
|
||||
./scripts/setup-env.sh --format systemd --file /etc/junhong/cmp-gray.env
|
||||
./scripts/setup-env.sh --systemd .env.gray
|
||||
|
||||
说明:
|
||||
不传参数时默认生成 .env.local。
|
||||
指定文件名适合灰度、预发、生产等部署环境,例如 .env.gray。
|
||||
默认格式为 shell,会生成 export KEY=VALUE。
|
||||
systemd 格式会生成 KEY=VALUE,可用于 systemd EnvironmentFile=。
|
||||
也可以继续使用 ENV_FILE=.env.gray ./scripts/setup-env.sh。
|
||||
EOF
|
||||
}
|
||||
|
||||
parse_args() {
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
show_usage
|
||||
exit 0
|
||||
;;
|
||||
-f|--file)
|
||||
if [ -z "${2:-}" ]; then
|
||||
print_error "--file 需要指定生成文件名"
|
||||
exit 1
|
||||
fi
|
||||
if [ "${ENV_FILE_SET_BY_ARG:-false}" = "true" ]; then
|
||||
print_error "只能指定一个生成文件名"
|
||||
show_usage
|
||||
exit 1
|
||||
fi
|
||||
ENV_FILE="$2"
|
||||
ENV_FILE_SET_BY_ARG="true"
|
||||
shift 2
|
||||
;;
|
||||
--format)
|
||||
if [ -z "${2:-}" ]; then
|
||||
print_error "--format 需要指定 shell 或 systemd"
|
||||
exit 1
|
||||
fi
|
||||
ENV_FORMAT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--format=*)
|
||||
ENV_FORMAT="${1#--format=}"
|
||||
shift
|
||||
;;
|
||||
--systemd)
|
||||
ENV_FORMAT="systemd"
|
||||
shift
|
||||
;;
|
||||
--file=*)
|
||||
if [ "${ENV_FILE_SET_BY_ARG:-false}" = "true" ]; then
|
||||
print_error "只能指定一个生成文件名"
|
||||
show_usage
|
||||
exit 1
|
||||
fi
|
||||
ENV_FILE="${1#--file=}"
|
||||
ENV_FILE_SET_BY_ARG="true"
|
||||
shift
|
||||
;;
|
||||
-*)
|
||||
print_error "未知参数: $1"
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
if [ "${ENV_FILE_SET_BY_ARG:-false}" = "true" ]; then
|
||||
print_error "只能指定一个生成文件名"
|
||||
show_usage
|
||||
exit 1
|
||||
fi
|
||||
ENV_FILE="$1"
|
||||
ENV_FILE_SET_BY_ARG="true"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$ENV_FILE" ]; then
|
||||
print_error "生成文件名不能为空"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$ENV_FORMAT" in
|
||||
shell|systemd)
|
||||
;;
|
||||
*)
|
||||
print_error "未知输出格式: $ENV_FORMAT(可选 shell 或 systemd)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 打印带颜色的消息
|
||||
print_header() {
|
||||
@@ -49,31 +153,50 @@ read_input() {
|
||||
local prompt="$1"
|
||||
local default="$2"
|
||||
local var_name="$3"
|
||||
local is_password="$4"
|
||||
|
||||
local is_password="${4:-false}"
|
||||
local input=""
|
||||
local display_default="$default"
|
||||
|
||||
if [ -z "$display_default" ]; then
|
||||
display_default="空"
|
||||
fi
|
||||
|
||||
if [ "$is_password" = "true" ]; then
|
||||
echo -n -e "${CYAN}$prompt${NC} [默认: ****]: "
|
||||
read -s input
|
||||
if [ -n "$default" ]; then
|
||||
display_default="****"
|
||||
fi
|
||||
echo -n -e "${CYAN}$prompt${NC} [默认: $display_default]: "
|
||||
IFS= read -r -s input
|
||||
echo ""
|
||||
else
|
||||
echo -n -e "${CYAN}$prompt${NC} [默认: $default]: "
|
||||
read input
|
||||
echo -n -e "${CYAN}$prompt${NC} [默认: $display_default]: "
|
||||
IFS= read -r input
|
||||
fi
|
||||
|
||||
|
||||
if [ -z "$input" ]; then
|
||||
eval "$var_name='$default'"
|
||||
printf -v "$var_name" '%s' "$default"
|
||||
else
|
||||
eval "$var_name='$input'"
|
||||
printf -v "$var_name" '%s' "$input"
|
||||
fi
|
||||
}
|
||||
|
||||
ask_yes_no() {
|
||||
local prompt="$1"
|
||||
local answer=""
|
||||
|
||||
echo -n "$prompt [y/N]: "
|
||||
IFS= read -r answer
|
||||
[ "$answer" = "y" ] || [ "$answer" = "Y" ]
|
||||
}
|
||||
|
||||
# 生成随机密钥
|
||||
generate_secret() {
|
||||
if command -v openssl &> /dev/null; then
|
||||
openssl rand -hex 32
|
||||
else
|
||||
# 备用方案:使用 /dev/urandom
|
||||
elif command -v xxd &> /dev/null; then
|
||||
head -c 32 /dev/urandom | xxd -p
|
||||
else
|
||||
head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n'
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -82,7 +205,7 @@ check_service() {
|
||||
local host="$1"
|
||||
local port="$2"
|
||||
local name="$3"
|
||||
|
||||
|
||||
if command -v nc &> /dev/null; then
|
||||
if nc -z "$host" "$port" 2>/dev/null; then
|
||||
print_success "$name ($host:$port) 连接正常"
|
||||
@@ -97,80 +220,402 @@ check_service() {
|
||||
fi
|
||||
}
|
||||
|
||||
shell_escape() {
|
||||
local value="$1"
|
||||
value=${value//\'/\'\\\'\'}
|
||||
printf "'%s'" "$value"
|
||||
}
|
||||
|
||||
systemd_escape() {
|
||||
local value="$1"
|
||||
value=${value//\\/\\\\}
|
||||
value=${value//\"/\\\"}
|
||||
printf '"%s"' "$value"
|
||||
}
|
||||
|
||||
env_value_escape() {
|
||||
case "$ENV_FORMAT" in
|
||||
systemd)
|
||||
systemd_escape "$1"
|
||||
;;
|
||||
*)
|
||||
shell_escape "$1"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
write_line() {
|
||||
printf '%s\n' "$1" >> "$ENV_FILE"
|
||||
}
|
||||
|
||||
write_blank() {
|
||||
printf '\n' >> "$ENV_FILE"
|
||||
}
|
||||
|
||||
write_section() {
|
||||
write_blank
|
||||
write_line "# ----------------------------------------------------------------------------"
|
||||
write_line "# $1"
|
||||
write_line "# ----------------------------------------------------------------------------"
|
||||
}
|
||||
|
||||
write_note() {
|
||||
write_line "# $1"
|
||||
}
|
||||
|
||||
write_export() {
|
||||
local name="$1"
|
||||
local value="$2"
|
||||
local description="$3"
|
||||
|
||||
write_note "$description"
|
||||
if [ "$ENV_FORMAT" = "systemd" ]; then
|
||||
printf '%s=%s\n' "$name" "$(env_value_escape "$value")" >> "$ENV_FILE"
|
||||
else
|
||||
printf 'export %s=%s\n' "$name" "$(env_value_escape "$value")" >> "$ENV_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
write_commented_export() {
|
||||
local name="$1"
|
||||
local value="$2"
|
||||
local description="$3"
|
||||
|
||||
write_note "$description"
|
||||
if [ "$ENV_FORMAT" = "systemd" ]; then
|
||||
printf '# %s=%s\n' "$name" "$(env_value_escape "$value")" >> "$ENV_FILE"
|
||||
else
|
||||
printf '# export %s=%s\n' "$name" "$(env_value_escape "$value")" >> "$ENV_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
init_env_file() {
|
||||
: > "$ENV_FILE"
|
||||
write_line "# =========================================================================="
|
||||
write_line "# 君鸿卡管系统 - 本地开发环境变量"
|
||||
write_line "# 生成时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
write_line "# 配置依据: pkg/config/defaults/config.yaml + pkg/config/loader.go"
|
||||
write_line "# 输出格式: $ENV_FORMAT"
|
||||
write_line "# =========================================================================="
|
||||
if [ "$ENV_FORMAT" = "systemd" ]; then
|
||||
write_line "# 使用方法:"
|
||||
write_line "# 在 systemd unit 中添加:EnvironmentFile=$ENV_FILE"
|
||||
write_line "# 然后执行:systemctl daemon-reload && systemctl restart <service>"
|
||||
else
|
||||
write_line "# 使用方法:"
|
||||
write_line "# source $ENV_FILE && go run cmd/api/main.go"
|
||||
write_line "# 或者:"
|
||||
write_line "# ./scripts/run-local.sh"
|
||||
fi
|
||||
write_line "#"
|
||||
write_line "# 说明:"
|
||||
if [ "$ENV_FORMAT" = "systemd" ]; then
|
||||
write_line "# - 未注释的 KEY=VALUE 是 systemd 会读取的配置。"
|
||||
write_line "# - 注释掉的 KEY=VALUE 是当前有默认值、按需启用或当前主流程未使用的配置。"
|
||||
else
|
||||
write_line "# - 未注释的 export 是本地启动常用或必填配置。"
|
||||
write_line "# - 注释掉的 export 是当前有默认值、按需启用或当前主流程未使用的配置。"
|
||||
fi
|
||||
write_line "# - 取消注释前请先阅读上方说明,避免覆盖嵌入配置默认值。"
|
||||
write_line "# =========================================================================="
|
||||
}
|
||||
|
||||
write_required_config() {
|
||||
write_section "数据库配置(必填)"
|
||||
write_export "JUNHONG_DATABASE_HOST" "$DB_HOST" "数据库主机地址;应用启动必填。"
|
||||
write_export "JUNHONG_DATABASE_PORT" "$DB_PORT" "数据库端口;默认 5432。"
|
||||
write_export "JUNHONG_DATABASE_USER" "$DB_USER" "数据库用户名;应用启动必填。"
|
||||
write_export "JUNHONG_DATABASE_PASSWORD" "$DB_PASSWORD" "数据库密码;应用启动必填,敏感信息请勿提交。"
|
||||
write_export "JUNHONG_DATABASE_DBNAME" "$DB_NAME" "数据库名称;应用启动必填。"
|
||||
write_export "JUNHONG_DATABASE_SSLMODE" "$DB_SSLMODE" "数据库 SSL 模式;本地通常使用 disable。"
|
||||
|
||||
write_section "Redis 配置(必填)"
|
||||
write_export "JUNHONG_REDIS_ADDRESS" "$REDIS_ADDRESS" "Redis 主机地址;应用启动必填。"
|
||||
write_export "JUNHONG_REDIS_PORT" "$REDIS_PORT" "Redis 端口;默认 6379。"
|
||||
write_export "JUNHONG_REDIS_PASSWORD" "$REDIS_PASSWORD" "Redis 密码;无密码时留空。"
|
||||
write_export "JUNHONG_REDIS_DB" "$REDIS_DB" "Redis 数据库编号;默认 0。"
|
||||
|
||||
write_section "JWT 配置(必填)"
|
||||
write_export "JUNHONG_JWT_SECRET_KEY" "$JWT_SECRET_KEY" "JWT 签名密钥;必须至少 32 字符,生产环境必须单独生成。"
|
||||
write_commented_export "JUNHONG_JWT_TOKEN_DURATION" "24h" "C 端 JWT Token 有效期;默认 24h,通常无需覆盖。"
|
||||
write_commented_export "JUNHONG_JWT_ACCESS_TOKEN_TTL" "24h" "B 端访问令牌 TTL;默认 24h。"
|
||||
write_commented_export "JUNHONG_JWT_REFRESH_TOKEN_TTL" "168h" "B 端刷新令牌 TTL;默认 168h。"
|
||||
}
|
||||
|
||||
write_common_config() {
|
||||
write_section "服务器配置"
|
||||
write_export "JUNHONG_SERVER_ADDRESS" "$SERVER_ADDRESS" "服务监听地址;本地默认 :3000。"
|
||||
write_commented_export "JUNHONG_SERVER_READ_TIMEOUT" "30s" "读取请求超时时间;默认 30s,通常无需覆盖。"
|
||||
write_commented_export "JUNHONG_SERVER_WRITE_TIMEOUT" "30s" "写响应超时时间;默认 30s,通常无需覆盖。"
|
||||
write_commented_export "JUNHONG_SERVER_SHUTDOWN_TIMEOUT" "30s" "优雅关闭超时时间;默认 30s。"
|
||||
write_commented_export "JUNHONG_SERVER_PREFORK" "false" "Fiber 预分叉模式;本地和 macOS 开发环境通常不要启用。"
|
||||
|
||||
write_section "日志配置"
|
||||
write_export "JUNHONG_LOGGING_LEVEL" "$LOGGING_LEVEL" "日志级别;可选 debug/info/warn/error。"
|
||||
write_export "JUNHONG_LOGGING_DEVELOPMENT" "true" "本地开发模式日志;生产环境应使用 false。"
|
||||
write_export "JUNHONG_LOGGING_APP_LOG_FILENAME" "logs/app.log" "应用日志文件路径;本地写入项目 logs 目录。"
|
||||
write_export "JUNHONG_LOGGING_ACCESS_LOG_FILENAME" "logs/access.log" "访问日志文件路径;本地写入项目 logs 目录。"
|
||||
write_commented_export "JUNHONG_LOGGING_APP_LOG_MAX_SIZE" "100" "应用日志单文件最大大小(MB);默认 100。"
|
||||
write_commented_export "JUNHONG_LOGGING_APP_LOG_MAX_BACKUPS" "3" "应用日志最多保留的旧文件数;默认 3。"
|
||||
write_commented_export "JUNHONG_LOGGING_APP_LOG_MAX_AGE" "7" "应用日志最多保留天数;默认 7。"
|
||||
write_commented_export "JUNHONG_LOGGING_APP_LOG_COMPRESS" "true" "是否压缩应用日志轮转文件;默认 true。"
|
||||
write_commented_export "JUNHONG_LOGGING_ACCESS_LOG_MAX_SIZE" "100" "访问日志单文件最大大小(MB);默认 100。"
|
||||
write_commented_export "JUNHONG_LOGGING_ACCESS_LOG_MAX_BACKUPS" "3" "访问日志最多保留的旧文件数;默认 3。"
|
||||
write_commented_export "JUNHONG_LOGGING_ACCESS_LOG_MAX_AGE" "7" "访问日志最多保留天数;默认 7。"
|
||||
write_commented_export "JUNHONG_LOGGING_ACCESS_LOG_COMPRESS" "true" "是否压缩访问日志轮转文件;默认 true。"
|
||||
|
||||
write_section "客户端配置"
|
||||
write_export "JUNHONG_CLIENT_REQUIRE_PHONE_BINDING" "true" "是否强制 C 端用户绑定手机号;当前默认 true。"
|
||||
}
|
||||
|
||||
write_tuning_config() {
|
||||
write_section "数据库连接池配置(可选)"
|
||||
write_commented_export "JUNHONG_DATABASE_MAX_OPEN_CONNS" "25" "数据库最大打开连接数;默认 25,本地通常不用改。"
|
||||
write_commented_export "JUNHONG_DATABASE_MAX_IDLE_CONNS" "10" "数据库最大空闲连接数;默认 10。"
|
||||
write_commented_export "JUNHONG_DATABASE_CONN_MAX_LIFETIME" "5m" "数据库连接最大生命周期;默认 5m。"
|
||||
|
||||
write_section "Redis 连接池配置(可选)"
|
||||
write_commented_export "JUNHONG_REDIS_POOL_SIZE" "10" "Redis 连接池大小;默认 10。"
|
||||
write_commented_export "JUNHONG_REDIS_MIN_IDLE_CONNS" "5" "Redis 最小空闲连接数;默认 5。"
|
||||
write_commented_export "JUNHONG_REDIS_DIAL_TIMEOUT" "5s" "Redis 建连超时;默认 5s。"
|
||||
write_commented_export "JUNHONG_REDIS_READ_TIMEOUT" "3s" "Redis 读取超时;默认 3s。"
|
||||
write_commented_export "JUNHONG_REDIS_WRITE_TIMEOUT" "3s" "Redis 写入超时;默认 3s。"
|
||||
|
||||
write_section "任务队列配置(可选)"
|
||||
write_commented_export "JUNHONG_QUEUE_CONCURRENCY" "10" "Worker 并发数;默认 10。"
|
||||
write_commented_export "JUNHONG_QUEUE_RETRY_MAX" "5" "任务最大重试次数;默认 5。"
|
||||
write_commented_export "JUNHONG_QUEUE_TIMEOUT" "10m" "单个任务超时时间;默认 10m。"
|
||||
write_note "队列权重 critical/default/low 当前由嵌入配置 queue.queues 管理,未绑定为环境变量。"
|
||||
|
||||
write_section "限流中间件配置(可选)"
|
||||
write_commented_export "JUNHONG_MIDDLEWARE_ENABLE_RATE_LIMITER" "false" "是否启用全局限流;默认关闭。"
|
||||
write_commented_export "JUNHONG_MIDDLEWARE_RATE_LIMITER_MAX" "100" "限流窗口内最大请求数;默认 100。"
|
||||
write_commented_export "JUNHONG_MIDDLEWARE_RATE_LIMITER_EXPIRATION" "1m" "限流时间窗口;默认 1m。"
|
||||
write_commented_export "JUNHONG_MIDDLEWARE_RATE_LIMITER_STORAGE" "memory" "限流存储后端;可选 memory 或 redis。"
|
||||
|
||||
write_section "跨域中间件配置(可选)"
|
||||
write_commented_export "JUNHONG_MIDDLEWARE_CORS_ENABLED" "true" "是否启用 CORS;默认启用。"
|
||||
write_commented_export "JUNHONG_MIDDLEWARE_CORS_ALLOW_ORIGINS" "*" "允许访问 API 的前端来源,* 表示全部来源。"
|
||||
write_commented_export "JUNHONG_MIDDLEWARE_CORS_ALLOW_METHODS" "GET,POST,PUT,PATCH,DELETE,OPTIONS" "允许跨域调用的 HTTP 方法。"
|
||||
write_commented_export "JUNHONG_MIDDLEWARE_CORS_ALLOW_HEADERS" "Origin,Content-Type,Accept,Authorization,X-Requested-With,X-Request-ID" "允许跨域携带的请求头。"
|
||||
write_commented_export "JUNHONG_MIDDLEWARE_CORS_ALLOW_CREDENTIALS" "false" "是否允许浏览器携带 Cookie 等凭证;来源为 * 时必须保持 false。"
|
||||
|
||||
write_section "轮询配置(可选)"
|
||||
write_commented_export "JUNHONG_POLLING_VERBOSE_LOG" "false" "是否输出轮询详细日志;默认 false,排查上游数据时再开启。"
|
||||
write_commented_export "JUNHONG_POLLING_AUTO_TRIGGER_ENABLE_AUTO_TRIGGER" "true" "是否启用 C 端实名自动触发;默认 true。"
|
||||
write_commented_export "JUNHONG_POLLING_AUTO_TRIGGER_AUTO_TRIGGER_SYSTEM_USER_ID" "1" "自动触发使用的系统用户 ID;生产建议改成专用平台账号。"
|
||||
|
||||
write_section "Worker 运行配置(可选)"
|
||||
write_commented_export "JUNHONG_WORKER_ROLE" "all" "Worker 角色;单实例默认 all,多实例可用 leader/consumer。"
|
||||
write_commented_export "JUNHONG_WORKER_INSTANCE_NAME" "" "Worker 实例名称;多实例部署时用于区分日志。"
|
||||
}
|
||||
|
||||
write_gateway_config() {
|
||||
write_section "Gateway 服务配置"
|
||||
if [ "$GATEWAY_CONFIGURED" = "true" ]; then
|
||||
write_export "JUNHONG_GATEWAY_BASE_URL" "$GATEWAY_BASE_URL" "Gateway API 基础 URL;已按本次输入覆盖嵌入默认值。"
|
||||
write_export "JUNHONG_GATEWAY_APP_ID" "$GATEWAY_APP_ID" "Gateway 应用 ID;敏感信息请勿提交。"
|
||||
write_export "JUNHONG_GATEWAY_APP_SECRET" "$GATEWAY_APP_SECRET" "Gateway 应用密钥;敏感信息请勿提交。"
|
||||
write_export "JUNHONG_GATEWAY_TIMEOUT" "$GATEWAY_TIMEOUT" "Gateway 请求超时时间(秒);有效范围 5-300。"
|
||||
else
|
||||
write_note "当前嵌入配置已有默认 Gateway 配置;只有需要覆盖环境差异时再取消注释。"
|
||||
write_commented_export "JUNHONG_GATEWAY_BASE_URL" "https://lplan.whjhft.com/openapi" "Gateway API 基础 URL;默认来自嵌入配置。"
|
||||
write_commented_export "JUNHONG_GATEWAY_APP_ID" "60bgt1X8i7AvXqkd" "Gateway 应用 ID;默认来自嵌入配置。"
|
||||
write_commented_export "JUNHONG_GATEWAY_APP_SECRET" "BZeQttaZQt0i73moF" "Gateway 应用密钥;默认来自嵌入配置,敏感信息请勿提交。"
|
||||
write_commented_export "JUNHONG_GATEWAY_TIMEOUT" "30" "Gateway 请求超时时间(秒);默认 30。"
|
||||
fi
|
||||
}
|
||||
|
||||
write_sms_config() {
|
||||
write_section "短信服务配置(可选)"
|
||||
if [ "$SMS_CONFIGURED" = "true" ]; then
|
||||
write_export "JUNHONG_SMS_GATEWAY_URL" "$SMS_GATEWAY_URL" "短信网关地址;配置后会初始化短信客户端。"
|
||||
write_export "JUNHONG_SMS_USERNAME" "$SMS_USERNAME" "短信账号用户名。"
|
||||
write_export "JUNHONG_SMS_PASSWORD" "$SMS_PASSWORD" "短信账号密码;敏感信息请勿提交。"
|
||||
write_export "JUNHONG_SMS_SIGNATURE" "$SMS_SIGNATURE" "短信签名,例如【君鸿】。"
|
||||
write_export "JUNHONG_SMS_TIMEOUT" "$SMS_TIMEOUT" "短信请求超时时间;有效范围 5s-60s。"
|
||||
else
|
||||
write_note "短信服务未配置时系统会跳过短信客户端初始化;需要短信验证码时再取消注释。"
|
||||
write_commented_export "JUNHONG_SMS_GATEWAY_URL" "" "短信网关地址;设置后 username/password/signature 也必须填写。"
|
||||
write_commented_export "JUNHONG_SMS_USERNAME" "" "短信账号用户名。"
|
||||
write_commented_export "JUNHONG_SMS_PASSWORD" "" "短信账号密码;敏感信息请勿提交。"
|
||||
write_commented_export "JUNHONG_SMS_SIGNATURE" "" "短信签名,例如【君鸿】。"
|
||||
write_commented_export "JUNHONG_SMS_TIMEOUT" "10s" "短信请求超时时间;默认 10s。"
|
||||
fi
|
||||
}
|
||||
|
||||
write_storage_config() {
|
||||
write_section "对象存储配置(可选)"
|
||||
if [ "$STORAGE_CONFIGURED" = "true" ]; then
|
||||
write_export "JUNHONG_STORAGE_PROVIDER" "s3" "对象存储提供商;当前仅支持 s3 兼容服务。"
|
||||
write_export "JUNHONG_STORAGE_TEMP_DIR" "/tmp/junhong-storage" "临时文件目录;用于导入导出等临时文件。"
|
||||
write_export "JUNHONG_STORAGE_S3_ENDPOINT" "$STORAGE_S3_ENDPOINT" "S3 端点;配置后会初始化对象存储服务。"
|
||||
write_export "JUNHONG_STORAGE_S3_REGION" "$STORAGE_S3_REGION" "S3 区域。"
|
||||
write_export "JUNHONG_STORAGE_S3_BUCKET" "$STORAGE_S3_BUCKET" "S3 存储桶名称。"
|
||||
write_export "JUNHONG_STORAGE_S3_ACCESS_KEY_ID" "$STORAGE_S3_ACCESS_KEY_ID" "S3 Access Key ID;敏感信息请勿提交。"
|
||||
write_export "JUNHONG_STORAGE_S3_SECRET_ACCESS_KEY" "$STORAGE_S3_SECRET_ACCESS_KEY" "S3 Secret Access Key;敏感信息请勿提交。"
|
||||
write_export "JUNHONG_STORAGE_S3_USE_SSL" "$STORAGE_S3_USE_SSL" "是否使用 SSL;本地兼容服务常用 false。"
|
||||
write_export "JUNHONG_STORAGE_S3_PATH_STYLE" "$STORAGE_S3_PATH_STYLE" "是否使用路径风格;兼容 S3 服务通常使用 true。"
|
||||
write_export "JUNHONG_STORAGE_PRESIGN_UPLOAD_EXPIRES" "15m" "预签名上传 URL 有效期;默认 15m。"
|
||||
write_export "JUNHONG_STORAGE_PRESIGN_DOWNLOAD_EXPIRES" "24h" "预签名下载 URL 有效期;默认 24h。"
|
||||
else
|
||||
write_note "未配置 S3 端点时系统会跳过对象存储初始化;导入导出文件能力需要启用。"
|
||||
write_commented_export "JUNHONG_STORAGE_PROVIDER" "s3" "对象存储提供商;当前仅支持 s3 兼容服务。"
|
||||
write_commented_export "JUNHONG_STORAGE_TEMP_DIR" "/tmp/junhong-storage" "临时文件目录;默认 /tmp/junhong-storage。"
|
||||
write_commented_export "JUNHONG_STORAGE_S3_ENDPOINT" "" "S3 端点;为空时对象存储服务不初始化。"
|
||||
write_commented_export "JUNHONG_STORAGE_S3_REGION" "" "S3 区域。"
|
||||
write_commented_export "JUNHONG_STORAGE_S3_BUCKET" "" "S3 存储桶名称。"
|
||||
write_commented_export "JUNHONG_STORAGE_S3_ACCESS_KEY_ID" "" "S3 Access Key ID;敏感信息请勿提交。"
|
||||
write_commented_export "JUNHONG_STORAGE_S3_SECRET_ACCESS_KEY" "" "S3 Secret Access Key;敏感信息请勿提交。"
|
||||
write_commented_export "JUNHONG_STORAGE_S3_USE_SSL" "false" "是否使用 SSL;默认 false。"
|
||||
write_commented_export "JUNHONG_STORAGE_S3_PATH_STYLE" "true" "是否使用路径风格;默认 true。"
|
||||
write_commented_export "JUNHONG_STORAGE_PRESIGN_UPLOAD_EXPIRES" "15m" "预签名上传 URL 有效期;默认 15m。"
|
||||
write_commented_export "JUNHONG_STORAGE_PRESIGN_DOWNLOAD_EXPIRES" "24h" "预签名下载 URL 有效期;默认 24h。"
|
||||
fi
|
||||
}
|
||||
|
||||
write_admin_config() {
|
||||
write_section "默认超级管理员配置(可选)"
|
||||
write_note "不配置时会使用 pkg/constants 中的默认超管账号;生产环境建议在首次启动前覆盖。"
|
||||
write_commented_export "JUNHONG_DEFAULT_ADMIN_USERNAME" "" "默认超级管理员用户名;留空则使用代码内置默认值 admin。"
|
||||
write_commented_export "JUNHONG_DEFAULT_ADMIN_PASSWORD" "" "默认超级管理员密码;留空则使用代码内置默认值 Admin@123456。"
|
||||
write_commented_export "JUNHONG_DEFAULT_ADMIN_PHONE" "" "默认超级管理员手机号;留空则使用代码内置默认值 13800000000。"
|
||||
}
|
||||
|
||||
write_migration_compat_config() {
|
||||
write_section "迁移工具兼容配置(用于 scripts/migrate.sh)"
|
||||
write_export "MIGRATIONS_DIR" "migrations" "迁移目录;保留给迁移工具兼容使用。"
|
||||
write_export "DB_HOST" "$DB_HOST" "兼容旧迁移脚本的数据库主机。"
|
||||
write_export "DB_PORT" "$DB_PORT" "兼容旧迁移脚本的数据库端口。"
|
||||
write_export "DB_USER" "$DB_USER" "兼容旧迁移脚本的数据库用户名。"
|
||||
write_export "DB_PASSWORD" "$DB_PASSWORD" "兼容旧迁移脚本的数据库密码。"
|
||||
write_export "DB_NAME" "$DB_NAME" "兼容旧迁移脚本的数据库名称。"
|
||||
write_export "DB_SSLMODE" "$DB_SSLMODE" "兼容旧迁移脚本的 SSL 模式。"
|
||||
}
|
||||
|
||||
generate_env_file() {
|
||||
init_env_file
|
||||
write_required_config
|
||||
write_common_config
|
||||
write_tuning_config
|
||||
write_gateway_config
|
||||
write_sms_config
|
||||
write_storage_config
|
||||
write_admin_config
|
||||
write_migration_compat_config
|
||||
}
|
||||
|
||||
warn_if_env_file_not_ignored() {
|
||||
if [ ! -f ".gitignore" ]; then
|
||||
return
|
||||
fi
|
||||
if [ "$ENV_FILE" = ".env.local" ] && ! grep -q "^\.env\.local$" .gitignore; then
|
||||
echo ".env.local" >> .gitignore
|
||||
print_info "已将 .env.local 添加到 .gitignore"
|
||||
return
|
||||
fi
|
||||
if [[ "$ENV_FILE" = /* ]]; then
|
||||
return
|
||||
fi
|
||||
if command -v git &> /dev/null && git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
|
||||
if ! git check-ignore -q -- "$ENV_FILE"; then
|
||||
print_warning "当前生成文件 $ENV_FILE 未被 Git 忽略,请勿提交敏感环境变量。"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 主流程
|
||||
main() {
|
||||
parse_args "$@"
|
||||
|
||||
print_header "君鸿卡管系统 - 环境变量配置向导"
|
||||
|
||||
echo "本脚本将帮助您配置本地开发所需的环境变量。"
|
||||
|
||||
echo "本脚本将根据当前配置系统生成本地开发环境变量文件。"
|
||||
echo "配置将保存到 $ENV_FILE 文件中。"
|
||||
echo "输出格式: $ENV_FORMAT"
|
||||
echo ""
|
||||
|
||||
|
||||
# 检查是否已存在配置文件
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
print_warning "发现已存在的配置文件: $ENV_FILE"
|
||||
echo -n "是否覆盖?[y/N]: "
|
||||
read confirm
|
||||
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
|
||||
if ! ask_yes_no "是否覆盖?"; then
|
||||
print_info "已取消。如需更新配置,请手动编辑 $ENV_FILE"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
# ========================================
|
||||
# 连通性检测配置
|
||||
# ========================================
|
||||
print_header "连通性检测(可选)"
|
||||
|
||||
if ask_yes_no "是否在填写配置后检测 PostgreSQL/Redis 连通性?"; then
|
||||
CHECK_SERVICES="true"
|
||||
else
|
||||
CHECK_SERVICES="false"
|
||||
print_info "已跳过连通性检测;适合灰度、预发、生产等内网地址配置。"
|
||||
fi
|
||||
|
||||
# ========================================
|
||||
# 数据库配置
|
||||
# ========================================
|
||||
print_header "数据库配置 (PostgreSQL)"
|
||||
|
||||
|
||||
read_input "数据库主机地址" "localhost" DB_HOST
|
||||
read_input "数据库端口" "5432" DB_PORT
|
||||
read_input "数据库用户名" "postgres" DB_USER
|
||||
read_input "数据库密码" "postgres" DB_PASSWORD "true"
|
||||
read_input "数据库名称" "junhong_cmp_dev" DB_NAME
|
||||
read_input "SSL 模式" "disable" DB_SSLMODE
|
||||
|
||||
|
||||
# 测试数据库连接
|
||||
echo ""
|
||||
check_service "$DB_HOST" "$DB_PORT" "PostgreSQL"
|
||||
|
||||
if [ "$CHECK_SERVICES" = "true" ]; then
|
||||
check_service "$DB_HOST" "$DB_PORT" "PostgreSQL" || true
|
||||
else
|
||||
print_info "跳过 PostgreSQL 连通性检测"
|
||||
fi
|
||||
|
||||
# ========================================
|
||||
# Redis 配置
|
||||
# ========================================
|
||||
print_header "Redis 配置"
|
||||
|
||||
|
||||
read_input "Redis 主机地址" "localhost" REDIS_ADDRESS
|
||||
read_input "Redis 端口" "6379" REDIS_PORT
|
||||
read_input "Redis 密码(无密码直接回车)" "" REDIS_PASSWORD "true"
|
||||
read_input "Redis 数据库编号" "0" REDIS_DB
|
||||
|
||||
|
||||
# 测试 Redis 连接
|
||||
echo ""
|
||||
check_service "$REDIS_ADDRESS" "$REDIS_PORT" "Redis"
|
||||
|
||||
if [ "$CHECK_SERVICES" = "true" ]; then
|
||||
check_service "$REDIS_ADDRESS" "$REDIS_PORT" "Redis" || true
|
||||
else
|
||||
print_info "跳过 Redis 连通性检测"
|
||||
fi
|
||||
|
||||
# ========================================
|
||||
# JWT 配置
|
||||
# ========================================
|
||||
print_header "JWT 配置"
|
||||
|
||||
|
||||
DEFAULT_SECRET=$(generate_secret)
|
||||
read_input "JWT 密钥(直接回车生成随机密钥)" "$DEFAULT_SECRET" JWT_SECRET_KEY "true"
|
||||
|
||||
|
||||
# ========================================
|
||||
# 服务器配置
|
||||
# 服务器和日志配置
|
||||
# ========================================
|
||||
print_header "服务器配置"
|
||||
|
||||
print_header "服务器与日志配置"
|
||||
|
||||
read_input "服务监听地址" ":3000" SERVER_ADDRESS
|
||||
read_input "日志级别 (debug/info/warn/error)" "debug" LOGGING_LEVEL
|
||||
|
||||
|
||||
# ========================================
|
||||
# Gateway 配置
|
||||
# ========================================
|
||||
print_header "Gateway 配置(可选)"
|
||||
print_header "Gateway 配置(可选覆盖)"
|
||||
|
||||
echo -n "是否配置 Gateway 服务?[y/N]: "
|
||||
read configure_gateway
|
||||
|
||||
if [ "$configure_gateway" = "y" ] || [ "$configure_gateway" = "Y" ]; then
|
||||
if ask_yes_no "是否覆盖 Gateway 服务配置?"; then
|
||||
read_input "Gateway API 基础 URL" "https://lplan.whjhft.com/openapi" GATEWAY_BASE_URL
|
||||
read_input "Gateway 应用 ID" "" GATEWAY_APP_ID
|
||||
read_input "Gateway 应用密钥" "" GATEWAY_APP_SECRET "true"
|
||||
@@ -181,230 +626,86 @@ main() {
|
||||
fi
|
||||
|
||||
# ========================================
|
||||
# 微信配置
|
||||
# 短信配置
|
||||
# ========================================
|
||||
print_header "微信配置(可选)"
|
||||
print_header "短信配置(可选)"
|
||||
|
||||
echo -n "是否配置微信公众号和支付?[y/N]: "
|
||||
read configure_wechat
|
||||
|
||||
if [ "$configure_wechat" = "y" ] || [ "$configure_wechat" = "Y" ]; then
|
||||
echo ""
|
||||
print_info ">>> 微信公众号配置"
|
||||
read_input "公众号 AppID" "" WECHAT_OFFICIAL_ACCOUNT_APP_ID
|
||||
read_input "公众号 AppSecret" "" WECHAT_OFFICIAL_ACCOUNT_APP_SECRET "true"
|
||||
read_input "服务器配置 Token(可选)" "" WECHAT_OFFICIAL_ACCOUNT_TOKEN
|
||||
read_input "消息加解密 Key(可选)" "" WECHAT_OFFICIAL_ACCOUNT_AES_KEY "true"
|
||||
read_input "OAuth 回调 URL(可选)" "" WECHAT_OFFICIAL_ACCOUNT_OAUTH_REDIRECT_URL
|
||||
|
||||
echo ""
|
||||
print_info ">>> 微信支付配置"
|
||||
read_input "支付 AppID(通常与公众号相同)" "$WECHAT_OFFICIAL_ACCOUNT_APP_ID" WECHAT_PAYMENT_APP_ID
|
||||
read_input "商户号" "" WECHAT_PAYMENT_MCH_ID
|
||||
read_input "APIv3 密钥(32位)" "" WECHAT_PAYMENT_API_V3_KEY "true"
|
||||
read_input "APIv2 密钥(可选)" "" WECHAT_PAYMENT_API_V2_KEY "true"
|
||||
read_input "商户证书路径" "/app/certs/apiclient_cert.pem" WECHAT_PAYMENT_CERT_PATH
|
||||
read_input "商户私钥路径" "/app/certs/apiclient_key.pem" WECHAT_PAYMENT_KEY_PATH
|
||||
read_input "证书序列号" "" WECHAT_PAYMENT_SERIAL_NO
|
||||
read_input "支付回调 URL" "" WECHAT_PAYMENT_NOTIFY_URL
|
||||
read_input "是否启用 HTTP 调试(true/false)" "false" WECHAT_PAYMENT_HTTP_DEBUG
|
||||
read_input "HTTP 请求超时时间" "30s" WECHAT_PAYMENT_TIMEOUT
|
||||
|
||||
WECHAT_CONFIGURED="true"
|
||||
if ask_yes_no "是否配置短信服务?"; then
|
||||
read_input "短信网关 URL" "" SMS_GATEWAY_URL
|
||||
read_input "短信账号用户名" "" SMS_USERNAME
|
||||
read_input "短信账号密码" "" SMS_PASSWORD "true"
|
||||
read_input "短信签名" "" SMS_SIGNATURE
|
||||
read_input "短信请求超时时间" "10s" SMS_TIMEOUT
|
||||
SMS_CONFIGURED="true"
|
||||
else
|
||||
WECHAT_CONFIGURED="false"
|
||||
SMS_CONFIGURED="false"
|
||||
fi
|
||||
|
||||
# ========================================
|
||||
# 可选:对象存储配置
|
||||
# 对象存储配置
|
||||
# ========================================
|
||||
print_header "对象存储配置(可选)"
|
||||
|
||||
echo -n "是否配置对象存储?[y/N]: "
|
||||
read configure_storage
|
||||
|
||||
if [ "$configure_storage" = "y" ] || [ "$configure_storage" = "Y" ]; then
|
||||
if ask_yes_no "是否配置对象存储?"; then
|
||||
read_input "S3 端点" "" STORAGE_S3_ENDPOINT
|
||||
read_input "S3 区域" "" STORAGE_S3_REGION
|
||||
read_input "S3 存储桶" "" STORAGE_S3_BUCKET
|
||||
read_input "S3 Access Key ID" "" STORAGE_S3_ACCESS_KEY_ID
|
||||
read_input "S3 Secret Access Key" "" STORAGE_S3_SECRET_ACCESS_KEY "true"
|
||||
read_input "是否使用 SSL(true/false)" "false" STORAGE_S3_USE_SSL
|
||||
read_input "是否使用路径风格(true/false)" "true" STORAGE_S3_PATH_STYLE
|
||||
STORAGE_CONFIGURED="true"
|
||||
else
|
||||
STORAGE_CONFIGURED="false"
|
||||
fi
|
||||
|
||||
|
||||
# ========================================
|
||||
# 生成配置文件
|
||||
# ========================================
|
||||
print_header "生成配置文件"
|
||||
|
||||
cat > "$ENV_FILE" << EOF
|
||||
# ============================================================================
|
||||
# 君鸿卡管系统 - 本地开发环境变量
|
||||
# 生成时间: $(date '+%Y-%m-%d %H:%M:%S')
|
||||
# ============================================================================
|
||||
# 使用方法:
|
||||
# source .env.local && go run cmd/api/main.go
|
||||
# 或者:
|
||||
# ./scripts/run-local.sh
|
||||
# ============================================================================
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 数据库配置(必填)
|
||||
# ----------------------------------------------------------------------------
|
||||
export JUNHONG_DATABASE_HOST="$DB_HOST"
|
||||
export JUNHONG_DATABASE_PORT="$DB_PORT"
|
||||
export JUNHONG_DATABASE_USER="$DB_USER"
|
||||
export JUNHONG_DATABASE_PASSWORD="$DB_PASSWORD"
|
||||
export JUNHONG_DATABASE_DBNAME="$DB_NAME"
|
||||
export JUNHONG_DATABASE_SSLMODE="$DB_SSLMODE"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Redis 配置(必填)
|
||||
# ----------------------------------------------------------------------------
|
||||
export JUNHONG_REDIS_ADDRESS="$REDIS_ADDRESS"
|
||||
export JUNHONG_REDIS_PORT="$REDIS_PORT"
|
||||
export JUNHONG_REDIS_PASSWORD="$REDIS_PASSWORD"
|
||||
export JUNHONG_REDIS_DB="$REDIS_DB"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# JWT 配置(必填)
|
||||
# ----------------------------------------------------------------------------
|
||||
export JUNHONG_JWT_SECRET_KEY="$JWT_SECRET_KEY"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 客户端配置(可选)
|
||||
# ----------------------------------------------------------------------------
|
||||
# 是否强制 C 端用户绑定手机号(true: 强制,false: 不强制)
|
||||
export JUNHONG_CLIENT_REQUIRE_PHONE_BINDING="true"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 服务器配置
|
||||
# ----------------------------------------------------------------------------
|
||||
export JUNHONG_SERVER_ADDRESS="$SERVER_ADDRESS"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 日志配置
|
||||
# ----------------------------------------------------------------------------
|
||||
export JUNHONG_LOGGING_LEVEL="$LOGGING_LEVEL"
|
||||
export JUNHONG_LOGGING_DEVELOPMENT="true"
|
||||
export JUNHONG_LOGGING_APP_LOG_FILENAME="logs/app.log"
|
||||
export JUNHONG_LOGGING_ACCESS_LOG_FILENAME="logs/access.log"
|
||||
|
||||
EOF
|
||||
|
||||
# 添加 Gateway 配置(如果配置了)
|
||||
if [ "$GATEWAY_CONFIGURED" = "true" ]; then
|
||||
cat >> "$ENV_FILE" << EOF
|
||||
# ----------------------------------------------------------------------------
|
||||
# Gateway 服务配置
|
||||
# ----------------------------------------------------------------------------
|
||||
export JUNHONG_GATEWAY_BASE_URL="$GATEWAY_BASE_URL"
|
||||
export JUNHONG_GATEWAY_APP_ID="$GATEWAY_APP_ID"
|
||||
export JUNHONG_GATEWAY_APP_SECRET="$GATEWAY_APP_SECRET"
|
||||
export JUNHONG_GATEWAY_TIMEOUT="$GATEWAY_TIMEOUT"
|
||||
|
||||
EOF
|
||||
fi
|
||||
|
||||
# 添加微信配置(如果配置了)
|
||||
if [ "$WECHAT_CONFIGURED" = "true" ]; then
|
||||
cat >> "$ENV_FILE" << EOF
|
||||
# ----------------------------------------------------------------------------
|
||||
# 微信公众号配置
|
||||
# ----------------------------------------------------------------------------
|
||||
export JUNHONG_WECHAT_OFFICIAL_ACCOUNT_APP_ID="$WECHAT_OFFICIAL_ACCOUNT_APP_ID"
|
||||
export JUNHONG_WECHAT_OFFICIAL_ACCOUNT_APP_SECRET="$WECHAT_OFFICIAL_ACCOUNT_APP_SECRET"
|
||||
export JUNHONG_WECHAT_OFFICIAL_ACCOUNT_TOKEN="$WECHAT_OFFICIAL_ACCOUNT_TOKEN"
|
||||
export JUNHONG_WECHAT_OFFICIAL_ACCOUNT_AES_KEY="$WECHAT_OFFICIAL_ACCOUNT_AES_KEY"
|
||||
export JUNHONG_WECHAT_OFFICIAL_ACCOUNT_OAUTH_REDIRECT_URL="$WECHAT_OFFICIAL_ACCOUNT_OAUTH_REDIRECT_URL"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 微信支付配置
|
||||
# ----------------------------------------------------------------------------
|
||||
export JUNHONG_WECHAT_PAYMENT_APP_ID="$WECHAT_PAYMENT_APP_ID"
|
||||
export JUNHONG_WECHAT_PAYMENT_MCH_ID="$WECHAT_PAYMENT_MCH_ID"
|
||||
export JUNHONG_WECHAT_PAYMENT_API_V3_KEY="$WECHAT_PAYMENT_API_V3_KEY"
|
||||
export JUNHONG_WECHAT_PAYMENT_API_V2_KEY="$WECHAT_PAYMENT_API_V2_KEY"
|
||||
export JUNHONG_WECHAT_PAYMENT_CERT_PATH="$WECHAT_PAYMENT_CERT_PATH"
|
||||
export JUNHONG_WECHAT_PAYMENT_KEY_PATH="$WECHAT_PAYMENT_KEY_PATH"
|
||||
export JUNHONG_WECHAT_PAYMENT_SERIAL_NO="$WECHAT_PAYMENT_SERIAL_NO"
|
||||
export JUNHONG_WECHAT_PAYMENT_NOTIFY_URL="$WECHAT_PAYMENT_NOTIFY_URL"
|
||||
export JUNHONG_WECHAT_PAYMENT_HTTP_DEBUG="$WECHAT_PAYMENT_HTTP_DEBUG"
|
||||
export JUNHONG_WECHAT_PAYMENT_TIMEOUT="$WECHAT_PAYMENT_TIMEOUT"
|
||||
|
||||
EOF
|
||||
fi
|
||||
|
||||
# 添加对象存储配置(如果配置了)
|
||||
if [ "$STORAGE_CONFIGURED" = "true" ]; then
|
||||
cat >> "$ENV_FILE" << EOF
|
||||
# ----------------------------------------------------------------------------
|
||||
# 对象存储配置
|
||||
# ----------------------------------------------------------------------------
|
||||
export JUNHONG_STORAGE_PROVIDER="s3"
|
||||
export JUNHONG_STORAGE_S3_ENDPOINT="$STORAGE_S3_ENDPOINT"
|
||||
export JUNHONG_STORAGE_S3_REGION="$STORAGE_S3_REGION"
|
||||
export JUNHONG_STORAGE_S3_BUCKET="$STORAGE_S3_BUCKET"
|
||||
export JUNHONG_STORAGE_S3_ACCESS_KEY_ID="$STORAGE_S3_ACCESS_KEY_ID"
|
||||
export JUNHONG_STORAGE_S3_SECRET_ACCESS_KEY="$STORAGE_S3_SECRET_ACCESS_KEY"
|
||||
export JUNHONG_STORAGE_S3_USE_SSL="false"
|
||||
export JUNHONG_STORAGE_S3_PATH_STYLE="true"
|
||||
|
||||
EOF
|
||||
fi
|
||||
|
||||
# 添加迁移工具兼容配置
|
||||
cat >> "$ENV_FILE" << EOF
|
||||
# ----------------------------------------------------------------------------
|
||||
# 迁移工具兼容配置(用于 scripts/migrate.sh)
|
||||
# ----------------------------------------------------------------------------
|
||||
export MIGRATIONS_DIR="migrations"
|
||||
export DB_HOST="$DB_HOST"
|
||||
export DB_PORT="$DB_PORT"
|
||||
export DB_USER="$DB_USER"
|
||||
export DB_PASSWORD="$DB_PASSWORD"
|
||||
export DB_NAME="$DB_NAME"
|
||||
export DB_SSLMODE="$DB_SSLMODE"
|
||||
EOF
|
||||
generate_env_file
|
||||
|
||||
print_success "配置文件已生成: $ENV_FILE"
|
||||
|
||||
|
||||
# ========================================
|
||||
# 后续步骤提示
|
||||
# ========================================
|
||||
print_header "设置完成!"
|
||||
|
||||
|
||||
echo -e "${GREEN}环境变量已配置完成!${NC}"
|
||||
echo ""
|
||||
echo "后续步骤:"
|
||||
echo ""
|
||||
echo " 1. 加载环境变量:"
|
||||
echo -e " ${CYAN}source .env.local${NC}"
|
||||
echo ""
|
||||
echo " 2. 运行数据库迁移(如果需要):"
|
||||
echo -e " ${CYAN}./scripts/migrate.sh up${NC}"
|
||||
echo ""
|
||||
echo " 3. 启动 API 服务:"
|
||||
echo -e " ${CYAN}go run cmd/api/main.go${NC}"
|
||||
echo ""
|
||||
echo " 4. 启动 Worker 服务(可选):"
|
||||
echo -e " ${CYAN}go run cmd/worker/main.go${NC}"
|
||||
echo ""
|
||||
echo " 或者使用快捷脚本:"
|
||||
echo -e " ${CYAN}./scripts/run-local.sh${NC}"
|
||||
echo ""
|
||||
print_warning "注意:.env.local 包含敏感信息,请勿提交到 Git!"
|
||||
|
||||
# 确保 .env.local 在 .gitignore 中
|
||||
if [ -f ".gitignore" ]; then
|
||||
if ! grep -q "^\.env\.local$" .gitignore; then
|
||||
echo ".env.local" >> .gitignore
|
||||
print_info "已将 .env.local 添加到 .gitignore"
|
||||
fi
|
||||
if [ "$ENV_FORMAT" = "systemd" ]; then
|
||||
echo " 1. 在 systemd unit 中引用:"
|
||||
echo -e " ${CYAN}EnvironmentFile=$ENV_FILE${NC}"
|
||||
echo ""
|
||||
echo " 2. 重载并重启服务:"
|
||||
echo -e " ${CYAN}systemctl daemon-reload && systemctl restart <service>${NC}"
|
||||
echo ""
|
||||
echo " 3. 需要手动迁移时,可临时生成 shell 格式文件或手动导出 DB_* 兼容变量。"
|
||||
else
|
||||
echo " 1. 加载环境变量:"
|
||||
echo -e " ${CYAN}source $ENV_FILE${NC}"
|
||||
echo ""
|
||||
echo " 2. 运行数据库迁移(如果需要):"
|
||||
echo -e " ${CYAN}./scripts/migrate.sh up${NC}"
|
||||
echo ""
|
||||
echo " 3. 启动 API 服务:"
|
||||
echo -e " ${CYAN}go run cmd/api/main.go${NC}"
|
||||
echo ""
|
||||
echo " 4. 启动 Worker 服务(可选):"
|
||||
echo -e " ${CYAN}go run cmd/worker/main.go${NC}"
|
||||
echo ""
|
||||
echo " 或者使用快捷脚本:"
|
||||
echo -e " ${CYAN}./scripts/run-local.sh${NC}"
|
||||
fi
|
||||
echo ""
|
||||
print_warning "注意:$ENV_FILE 包含敏感信息,请勿提交到 Git!"
|
||||
|
||||
warn_if_env_file_not_ignored
|
||||
}
|
||||
|
||||
# 运行主流程
|
||||
main
|
||||
main "$@"
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/config"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/storage"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
fmt.Printf("加载配置失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
provider, err := storage.NewS3Provider(&cfg.Storage)
|
||||
if err != nil {
|
||||
fmt.Printf("创建 S3 Provider 失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
testContent := "iccid,msisdn\n89860012345678901234,13800000001\n89860012345678901235,13800000002\n"
|
||||
testKey := fmt.Sprintf("test/verify-%d.csv", time.Now().Unix())
|
||||
|
||||
fmt.Printf("上传文件到: %s/%s\n", cfg.Storage.S3.Bucket, testKey)
|
||||
|
||||
if err := provider.Upload(ctx, testKey, bytes.NewReader([]byte(testContent)), "text/csv"); err != nil {
|
||||
fmt.Printf("上传失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
exists, err := provider.Exists(ctx, testKey)
|
||||
if err != nil {
|
||||
fmt.Printf("检查存在失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("上传完成,文件存在: %v\n", exists)
|
||||
fmt.Printf("\n请在联通云后台检查 bucket '%s' 下的文件: %s\n", cfg.Storage.S3.Bucket, testKey)
|
||||
fmt.Println("文件未删除,请手动验证后删除")
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 微信配置验证脚本
|
||||
# 用途:检查微信公众号和支付配置的完整性
|
||||
|
||||
set -e
|
||||
|
||||
echo "========================================"
|
||||
echo " 微信配置验证脚本"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 错误计数
|
||||
ERROR_COUNT=0
|
||||
WARNING_COUNT=0
|
||||
|
||||
# 检查环境变量是否存在
|
||||
check_env() {
|
||||
local var_name=$1
|
||||
local is_required=${2:-true}
|
||||
|
||||
if [ -z "${!var_name}" ]; then
|
||||
if [ "$is_required" = true ]; then
|
||||
echo -e "${RED}✗ 缺失必填配置: $var_name${NC}"
|
||||
((ERROR_COUNT++))
|
||||
return 1
|
||||
else
|
||||
echo -e "${YELLOW}⚠ 缺失可选配置: $var_name${NC}"
|
||||
((WARNING_COUNT++))
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN}✓ $var_name${NC}"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# 检查文件是否存在
|
||||
check_file() {
|
||||
local file_path=$1
|
||||
local var_name=$2
|
||||
|
||||
if [ ! -f "$file_path" ]; then
|
||||
echo -e "${RED}✗ 文件不存在: $file_path (来自 $var_name)${NC}"
|
||||
((ERROR_COUNT++))
|
||||
return 1
|
||||
else
|
||||
echo -e "${GREEN}✓ 文件存在: $file_path${NC}"
|
||||
|
||||
# 检查文件权限
|
||||
local perms=$(stat -f "%A" "$file_path" 2>/dev/null || stat -c "%a" "$file_path" 2>/dev/null)
|
||||
if [ "$perms" != "600" ] && [ "$perms" != "644" ] && [ "$perms" != "400" ]; then
|
||||
echo -e "${YELLOW} ⚠ 建议修改文件权限为 600: chmod 600 $file_path${NC}"
|
||||
((WARNING_COUNT++))
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# 检查字符串长度
|
||||
check_length() {
|
||||
local var_name=$1
|
||||
local expected_length=$2
|
||||
local value="${!var_name}"
|
||||
|
||||
if [ ${#value} -ne $expected_length ]; then
|
||||
echo -e "${YELLOW} ⚠ $var_name 长度应为 $expected_length 位,当前 ${#value} 位${NC}"
|
||||
((WARNING_COUNT++))
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
echo "1. 检查微信公众号配置"
|
||||
echo "----------------------------------------"
|
||||
check_env "JUNHONG_WECHAT_OFFICIAL_ACCOUNT_APP_ID" true
|
||||
check_env "JUNHONG_WECHAT_OFFICIAL_ACCOUNT_APP_SECRET" true
|
||||
check_env "JUNHONG_WECHAT_OFFICIAL_ACCOUNT_TOKEN" false
|
||||
check_env "JUNHONG_WECHAT_OFFICIAL_ACCOUNT_AES_KEY" false
|
||||
check_env "JUNHONG_WECHAT_OFFICIAL_ACCOUNT_OAUTH_REDIRECT_URL" false
|
||||
echo ""
|
||||
|
||||
echo "2. 检查微信支付配置"
|
||||
echo "----------------------------------------"
|
||||
check_env "JUNHONG_WECHAT_PAYMENT_APP_ID" true
|
||||
check_env "JUNHONG_WECHAT_PAYMENT_MCH_ID" true
|
||||
check_env "JUNHONG_WECHAT_PAYMENT_API_V3_KEY" true
|
||||
check_env "JUNHONG_WECHAT_PAYMENT_API_V2_KEY" false
|
||||
check_env "JUNHONG_WECHAT_PAYMENT_CERT_PATH" true
|
||||
check_env "JUNHONG_WECHAT_PAYMENT_KEY_PATH" true
|
||||
check_env "JUNHONG_WECHAT_PAYMENT_SERIAL_NO" true
|
||||
check_env "JUNHONG_WECHAT_PAYMENT_NOTIFY_URL" true
|
||||
check_env "JUNHONG_WECHAT_PAYMENT_HTTP_DEBUG" false
|
||||
check_env "JUNHONG_WECHAT_PAYMENT_TIMEOUT" false
|
||||
echo ""
|
||||
|
||||
echo "3. 检查证书文件"
|
||||
echo "----------------------------------------"
|
||||
if [ -n "$JUNHONG_WECHAT_PAYMENT_CERT_PATH" ]; then
|
||||
check_file "$JUNHONG_WECHAT_PAYMENT_CERT_PATH" "JUNHONG_WECHAT_PAYMENT_CERT_PATH"
|
||||
fi
|
||||
|
||||
if [ -n "$JUNHONG_WECHAT_PAYMENT_KEY_PATH" ]; then
|
||||
check_file "$JUNHONG_WECHAT_PAYMENT_KEY_PATH" "JUNHONG_WECHAT_PAYMENT_KEY_PATH"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "4. 验证配置格式"
|
||||
echo "----------------------------------------"
|
||||
|
||||
# 检查 AppID 格式(应以 wx 开头)
|
||||
if [ -n "$JUNHONG_WECHAT_OFFICIAL_ACCOUNT_APP_ID" ]; then
|
||||
if [[ ! "$JUNHONG_WECHAT_OFFICIAL_ACCOUNT_APP_ID" =~ ^wx ]]; then
|
||||
echo -e "${YELLOW} ⚠ 公众号 AppID 格式可能有误(通常以 wx 开头)${NC}"
|
||||
((WARNING_COUNT++))
|
||||
fi
|
||||
fi
|
||||
|
||||
# 检查 APIv3 密钥长度(应为 32 位)
|
||||
if [ -n "$JUNHONG_WECHAT_PAYMENT_API_V3_KEY" ]; then
|
||||
check_length "JUNHONG_WECHAT_PAYMENT_API_V3_KEY" 32
|
||||
fi
|
||||
|
||||
# 检查回调 URL 格式(必须是 HTTPS)
|
||||
if [ -n "$JUNHONG_WECHAT_PAYMENT_NOTIFY_URL" ]; then
|
||||
if [[ ! "$JUNHONG_WECHAT_PAYMENT_NOTIFY_URL" =~ ^https:// ]]; then
|
||||
echo -e "${RED}✗ 支付回调 URL 必须使用 HTTPS${NC}"
|
||||
((ERROR_COUNT++))
|
||||
else
|
||||
echo -e "${GREEN}✓ 支付回调 URL 使用 HTTPS${NC}"
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "5. 检查证书有效性(可选)"
|
||||
echo "----------------------------------------"
|
||||
if [ -n "$JUNHONG_WECHAT_PAYMENT_CERT_PATH" ] && [ -f "$JUNHONG_WECHAT_PAYMENT_CERT_PATH" ]; then
|
||||
if command -v openssl &> /dev/null; then
|
||||
# 检查证书是否过期
|
||||
expiry_date=$(openssl x509 -in "$JUNHONG_WECHAT_PAYMENT_CERT_PATH" -noout -enddate 2>/dev/null | cut -d= -f2)
|
||||
if [ -n "$expiry_date" ]; then
|
||||
echo -e "${GREEN}✓ 证书有效期至: $expiry_date${NC}"
|
||||
|
||||
# 检查证书序列号是否匹配
|
||||
cert_serial=$(openssl x509 -in "$JUNHONG_WECHAT_PAYMENT_CERT_PATH" -noout -serial 2>/dev/null | cut -d= -f2)
|
||||
if [ -n "$cert_serial" ]; then
|
||||
if [ "$cert_serial" != "$JUNHONG_WECHAT_PAYMENT_SERIAL_NO" ]; then
|
||||
echo -e "${YELLOW} ⚠ 证书序列号不匹配${NC}"
|
||||
echo -e " 配置中: $JUNHONG_WECHAT_PAYMENT_SERIAL_NO"
|
||||
echo -e " 证书中: $cert_serial"
|
||||
((WARNING_COUNT++))
|
||||
else
|
||||
echo -e "${GREEN} ✓ 证书序列号匹配${NC}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW} ⚠ 未安装 openssl,跳过证书验证${NC}"
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "========================================"
|
||||
echo " 验证结果"
|
||||
echo "========================================"
|
||||
echo -e "${RED}错误: $ERROR_COUNT${NC}"
|
||||
echo -e "${YELLOW}警告: $WARNING_COUNT${NC}"
|
||||
echo ""
|
||||
|
||||
if [ $ERROR_COUNT -gt 0 ]; then
|
||||
echo -e "${RED}❌ 配置验证失败,请修复上述错误后重试${NC}"
|
||||
echo ""
|
||||
echo "建议操作:"
|
||||
echo "1. 检查 .env.local 文件是否正确加载"
|
||||
echo "2. 确认所有必填环境变量已设置"
|
||||
echo "3. 验证证书文件路径是否正确"
|
||||
echo "4. 参考文档: docs/wechat-integration/使用指南.md"
|
||||
exit 1
|
||||
elif [ $WARNING_COUNT -gt 0 ]; then
|
||||
echo -e "${YELLOW}⚠️ 配置验证通过,但存在警告${NC}"
|
||||
echo ""
|
||||
echo "建议操作:"
|
||||
echo "1. 检查警告信息并根据建议调整"
|
||||
echo "2. 警告不会影响服务启动,但可能影响功能"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${GREEN}✅ 配置验证通过,所有配置正确${NC}"
|
||||
echo ""
|
||||
echo "下一步:"
|
||||
echo "1. 启动服务: go run cmd/api/main.go"
|
||||
echo "2. 查看启动日志确认微信服务初始化成功"
|
||||
echo "3. 参考验证指南进行功能测试: docs/wechat-integration/验证指南.md"
|
||||
exit 0
|
||||
fi
|
||||
Reference in New Issue
Block a user