让七月迭代具备可直接部署的配置基线

补齐三套环境配置、测试环境部署门禁与 system_config 初始化,并按当前无企微应用的约束将企微凭据改为明文存储。

Constraint: 当前测试环境尚无企微应用及历史企微密文数据

Rejected: 使用启动环境变量加密企微凭据 | 用户明确要求直接明文保存并移除加密密钥

Confidence: high

Scope-risk: moderate

Directive: 企微真实闭环完成前保持两个旧审批入口开关为 true

Tested: gofmt;git diff --check;bash -n;docker compose config;Gitea workflow YAML 解析;OpenAPI 重新生成

Not-tested: go test;常规 go build;LSP;实际数据库迁移;真实测试环境部署;企微真实联调
This commit is contained in:
2026-07-25 18:18:45 +08:00
parent 73f5125d3d
commit cb26217205
29 changed files with 277 additions and 232 deletions

View File

@@ -32,23 +32,16 @@ type DefaultCreatorMemberFinder interface {
GetVisible(ctx context.Context, applicationID uint, userID string) (*model.WeComMember, error)
}
// CredentialCipher 定义企业微信敏感凭据加密边界。
type CredentialCipher interface {
Encrypt(plaintext string) ([]byte, error)
Decrypt(ciphertext []byte) (string, error)
}
// AccessTokenProvider 定义按应用取得及失效 access_token 的边界。
type AccessTokenProvider interface {
GetAccessToken(ctx context.Context, applicationID uint) (string, error)
Invalidate(ctx context.Context, applicationID uint)
}
// ConnectionService 保存加密配置并测试企业微信连接。
// ConnectionService 保存应用配置并测试企业微信连接。
type ConnectionService struct {
db *gorm.DB
repo ApplicationRepository
cipher CredentialCipher
tokens AccessTokenProvider
audit systemconfigapp.AuditWriter
members DefaultCreatorMemberFinder
@@ -61,14 +54,14 @@ func (s *ConnectionService) SetDefaultCreatorMemberFinder(finder DefaultCreatorM
}
// NewConnectionService 创建企业微信连接用例。
func NewConnectionService(db *gorm.DB, repo ApplicationRepository, cipher CredentialCipher, tokens AccessTokenProvider, audit systemconfigapp.AuditWriter) *ConnectionService {
return &ConnectionService{db: db, repo: repo, cipher: cipher, tokens: tokens, audit: audit, now: time.Now}
func NewConnectionService(db *gorm.DB, repo ApplicationRepository, tokens AccessTokenProvider, audit systemconfigapp.AuditWriter) *ConnectionService {
return &ConnectionService{db: db, repo: repo, tokens: tokens, audit: audit, now: time.Now}
}
// Save 创建或更新企业微信应用,并保证数据库仅保存密文凭据
// Save 创建或更新企业微信应用配置
func (s *ConnectionService) Save(ctx context.Context, request dto.SaveWeComApplicationRequest) (*dto.WeComApplicationResponse, error) {
if s == nil || s.db == nil || s.repo == nil || s.cipher == nil {
return nil, errors.New(errors.CodeWeComCredentialInvalid)
if s == nil || s.db == nil || s.repo == nil {
return nil, errors.New(errors.CodeServiceUnavailable, "企业微信连接服务未配置")
}
if middleware.GetUserTypeFromContext(ctx) != constants.UserTypeSuperAdmin {
return nil, errors.New(errors.CodeForbidden)
@@ -77,21 +70,9 @@ func (s *ConnectionService) Save(ctx context.Context, request dto.SaveWeComAppli
if operatorID == 0 {
return nil, errors.New(errors.CodeInvalidParam)
}
secret, err := s.cipher.Encrypt(request.Secret)
if err != nil {
return nil, err
}
callbackToken, err := s.cipher.Encrypt(request.CallbackToken)
if err != nil {
return nil, err
}
encodingAESKey, err := s.cipher.Encrypt(request.EncodingAESKey)
if err != nil {
return nil, err
}
now := s.now().UTC()
var saved *model.WeComApplication
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Exec("SELECT pg_advisory_xact_lock(hashtext(?))", fmt.Sprintf("wecom:%s:%d", request.CorpID, request.AgentID)).Error; err != nil {
return err
}
@@ -111,9 +92,9 @@ func (s *ConnectionService) Save(ctx context.Context, request dto.SaveWeComAppli
before = applicationAuditSnapshot(existing)
}
existing.Name = request.Name
existing.SecretCiphertext = secret
existing.CallbackTokenCiphertext = callbackToken
existing.EncodingAESKeyCiphertext = encodingAESKey
existing.Secret = request.Secret
existing.CallbackToken = request.CallbackToken
existing.EncodingAESKey = request.EncodingAESKey
existing.Status = request.Status
existing.UpdatedBy = operatorID
existing.UpdatedAt = now
@@ -150,14 +131,14 @@ func (s *ConnectionService) Save(ctx context.Context, request dto.SaveWeComAppli
if s.tokens != nil {
s.tokens.Invalidate(ctx, saved.ID)
}
response := toApplicationResponse(*saved, request.Secret, request.CallbackToken, request.EncodingAESKey)
response := toApplicationResponse(*saved)
return &response, nil
}
// List 返回企业微信应用列表,并向超级管理员返回可直接编辑的明文凭据。
// List 返回企业微信应用列表,并向超级管理员返回可直接编辑的凭据。
func (s *ConnectionService) List(ctx context.Context, request dto.WeComApplicationListRequest) (*dto.WeComApplicationListResponse, error) {
if s == nil || s.repo == nil || s.cipher == nil {
return nil, errors.New(errors.CodeWeComCredentialInvalid)
if s == nil || s.repo == nil {
return nil, errors.New(errors.CodeServiceUnavailable, "企业微信连接服务未配置")
}
if middleware.GetUserTypeFromContext(ctx) != constants.UserTypeSuperAdmin {
return nil, errors.New(errors.CodeForbidden)
@@ -177,19 +158,7 @@ func (s *ConnectionService) List(ctx context.Context, request dto.WeComApplicati
}
result := make([]dto.WeComApplicationResponse, 0, len(applications))
for _, application := range applications {
secret, err := s.cipher.Decrypt(application.SecretCiphertext)
if err != nil {
return nil, err
}
callbackToken, err := s.cipher.Decrypt(application.CallbackTokenCiphertext)
if err != nil {
return nil, err
}
encodingAESKey, err := s.cipher.Decrypt(application.EncodingAESKeyCiphertext)
if err != nil {
return nil, err
}
result = append(result, toApplicationResponse(application, secret, callbackToken, encodingAESKey))
result = append(result, toApplicationResponse(application))
}
return &dto.WeComApplicationListResponse{
Items: result, Total: total, Page: request.Page, PageSize: request.PageSize,
@@ -214,7 +183,7 @@ func (s *ConnectionService) Test(ctx context.Context, applicationID uint) error
// SaveDefaultCreator 从应用当前可见成员中保存代理等账号使用的默认审批发起人。
func (s *ConnectionService) SaveDefaultCreator(ctx context.Context, applicationID uint, request dto.SaveWeComDefaultCreatorRequest) (*dto.WeComApplicationResponse, error) {
if s == nil || s.db == nil || s.repo == nil || s.cipher == nil || s.members == nil {
if s == nil || s.db == nil || s.repo == nil || s.members == nil {
return nil, errors.New(errors.CodeServiceUnavailable, "企业微信默认审批发起人服务未配置")
}
if middleware.GetUserTypeFromContext(ctx) != constants.UserTypeSuperAdmin {
@@ -262,19 +231,7 @@ func (s *ConnectionService) SaveDefaultCreator(ctx context.Context, applicationI
}
return nil, errors.Wrap(errors.CodeDatabaseError, err, "保存企业微信默认审批发起人失败")
}
secret, err := s.cipher.Decrypt(application.SecretCiphertext)
if err != nil {
return nil, err
}
callbackToken, err := s.cipher.Decrypt(application.CallbackTokenCiphertext)
if err != nil {
return nil, err
}
encodingAESKey, err := s.cipher.Decrypt(application.EncodingAESKeyCiphertext)
if err != nil {
return nil, err
}
response := toApplicationResponse(*application, secret, callbackToken, encodingAESKey)
response := toApplicationResponse(*application)
return &response, nil
}
@@ -287,17 +244,17 @@ func applicationAuditSnapshot(application *model.WeComApplication) map[string]an
}
}
func toApplicationResponse(application model.WeComApplication, secret, callbackToken, encodingAESKey string) dto.WeComApplicationResponse {
func toApplicationResponse(application model.WeComApplication) dto.WeComApplicationResponse {
statusName := "禁用"
if application.Status == constants.StatusEnabled {
statusName = "启用"
}
return dto.WeComApplicationResponse{
ID: application.ID, CorpID: application.CorpID, AgentID: application.AgentID, Name: application.Name,
Secret: secret, CallbackToken: callbackToken, EncodingAESKey: encodingAESKey,
Secret: application.Secret, CallbackToken: application.CallbackToken, EncodingAESKey: application.EncodingAESKey,
DefaultCreatorUserID: application.DefaultCreatorUserID, DefaultCreatorName: application.DefaultCreatorName,
Status: application.Status, StatusName: statusName,
CredentialsSet: len(application.SecretCiphertext) > 0 && len(application.CallbackTokenCiphertext) > 0 && len(application.EncodingAESKeyCiphertext) > 0,
CredentialsSet: application.Secret != "" && application.CallbackToken != "" && application.EncodingAESKey != "",
LastConnectedAt: application.LastConnectedAt, CreatedAt: application.CreatedAt, UpdatedAt: application.UpdatedAt,
}
}

View File

@@ -31,7 +31,6 @@ import (
"github.com/break/junhong_cmp_fiber/pkg/config"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/go-playground/validator/v10"
"go.uber.org/zap"
)
func initHandlers(svc *services, deps *Dependencies) *Handlers {
@@ -111,27 +110,18 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
deps.DB, systemConfigRegistry, systemConfigCache, deps.SystemConfigAudit, systemConfigAlerts, nil,
)
wecomRepository := wecomInfra.NewApplicationRepository(deps.DB)
var credentialCipher *wecomInfra.CredentialCipher
wecomBaseURL := ""
wecomTimeout := constants.WeComDefaultHTTPTimeout
if cfg := config.Get(); cfg != nil {
wecomBaseURL = cfg.WeCom.BaseURL
wecomTimeout = cfg.WeCom.Timeout
cipher, err := wecomInfra.NewCredentialCipher(cfg.WeCom.CredentialEncryptionKey)
if err != nil {
if deps.Logger != nil {
deps.Logger.Warn("企业微信凭据加密密钥未配置或无效,相关接口将拒绝业务调用", zap.Error(err))
}
} else {
credentialCipher = cipher
}
}
wecomTokens := wecomInfra.NewTokenProvider(
wecomRepository, credentialCipher, deps.Redis, integrationlog.NewRepository(deps.DB),
wecomRepository, deps.Redis, integrationlog.NewRepository(deps.DB),
wecomBaseURL, wecomTimeout, deps.Logger,
)
wecomConnections := wecomApp.NewConnectionService(
deps.DB, wecomRepository, credentialCipher, wecomTokens, deps.SystemConfigAudit,
deps.DB, wecomRepository, wecomTokens, deps.SystemConfigAudit,
)
wecomMembers := wecomInfra.NewMemberRepository(deps.DB)
wecomConnections.SetDefaultCreatorMemberFinder(wecomMembers)
@@ -146,7 +136,7 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
wecomInfra.NewSceneRepository(deps.DB), deps.SystemConfigAudit,
)
wecomApprovalCallback := callback.NewWeComApprovalHandler(wecomInfra.NewCallbackService(
wecomRepository, credentialCipher, integrationlog.NewRepository(deps.DB), deps.QueueClient,
wecomRepository, integrationlog.NewRepository(deps.DB), deps.QueueClient,
))
svc.Account.SetWeComMemberFinder(wecomMembers)

View File

@@ -291,15 +291,13 @@ func initServices(s *stores, deps *Dependencies) *services {
wecomMemberRepository := wecomInfra.NewMemberRepository(deps.DB)
wecomBaseURL := ""
wecomTimeout := constants.WeComDefaultHTTPTimeout
var wecomCredentialCipher *wecomInfra.CredentialCipher
if cfg := config.Get(); cfg != nil {
wecomBaseURL = cfg.WeCom.BaseURL
wecomTimeout = cfg.WeCom.Timeout
wecomCredentialCipher, _ = wecomInfra.NewCredentialCipher(cfg.WeCom.CredentialEncryptionKey)
}
wecomIntegrationRepository := integrationlog.NewRepository(deps.DB)
wecomTokenProvider := wecomInfra.NewTokenProvider(
wecomApplicationRepository, wecomCredentialCipher, deps.Redis, wecomIntegrationRepository,
wecomApplicationRepository, deps.Redis, wecomIntegrationRepository,
wecomBaseURL, wecomTimeout, deps.Logger,
)
approvalCreationService := approvalApp.NewCreationService(

View File

@@ -44,15 +44,15 @@ func (r *ApplicationRepository) Create(ctx context.Context, tx *gorm.DB, applica
return nil
}
// Update 更新企业微信应用配置及密文凭据。
// Update 更新企业微信应用配置及连接凭据。
func (r *ApplicationRepository) Update(ctx context.Context, tx *gorm.DB, application *model.WeComApplication) error {
updates := map[string]any{
"name": application.Name, "secret_ciphertext": application.SecretCiphertext,
"callback_token_ciphertext": application.CallbackTokenCiphertext,
"encoding_aes_key_ciphertext": application.EncodingAESKeyCiphertext,
"default_creator_userid": application.DefaultCreatorUserID,
"default_creator_name": application.DefaultCreatorName,
"status": application.Status, "updated_by": application.UpdatedBy, "updated_at": application.UpdatedAt,
"name": application.Name, "secret": application.Secret,
"callback_token": application.CallbackToken,
"encoding_aes_key": application.EncodingAESKey,
"default_creator_userid": application.DefaultCreatorUserID,
"default_creator_name": application.DefaultCreatorName,
"status": application.Status, "updated_by": application.UpdatedBy, "updated_at": application.UpdatedAt,
}
if err := tx.WithContext(ctx).Model(&model.WeComApplication{}).Where("id = ?", application.ID).Updates(updates).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新企业微信应用配置失败")
@@ -73,7 +73,7 @@ func (r *ApplicationRepository) UpdateDefaultCreator(ctx context.Context, tx *go
return nil
}
// List 分页查询企业微信应用配置及密文凭据。
// List 分页查询企业微信应用配置及连接凭据。
func (r *ApplicationRepository) List(ctx context.Context, page, pageSize int) ([]model.WeComApplication, int64, error) {
query := r.db.WithContext(ctx).Model(&model.WeComApplication{})
var total int64
@@ -87,7 +87,7 @@ func (r *ApplicationRepository) List(ctx context.Context, page, pageSize int) ([
return applications, total, nil
}
// GetEnabled 查询启用的企业微信应用及密文凭据。
// GetEnabled 查询启用的企业微信应用及连接凭据。
func (r *ApplicationRepository) GetEnabled(ctx context.Context, applicationID uint) (*model.WeComApplication, error) {
var application model.WeComApplication
if err := r.db.WithContext(ctx).Where("id = ? AND status = ?", applicationID, constants.StatusEnabled).First(&application).Error; err != nil {

View File

@@ -40,14 +40,13 @@ type ApprovalDetailSyncTask struct {
// CallbackService 校验解密企微回调、记录入站幂等事实并异步拉取详情。
type CallbackService struct {
applications *ApplicationRepository
cipher *CredentialCipher
integration callbackIntegrationLog
queue *queue.Client
}
// NewCallbackService 创建企业微信审批回调服务。
func NewCallbackService(applications *ApplicationRepository, cipher *CredentialCipher, integration callbackIntegrationLog, queueClient *queue.Client) *CallbackService {
return &CallbackService{applications: applications, cipher: cipher, integration: integration, queue: queueClient}
func NewCallbackService(applications *ApplicationRepository, integration callbackIntegrationLog, queueClient *queue.Client) *CallbackService {
return &CallbackService{applications: applications, integration: integration, queue: queueClient}
}
// VerifyURL 校验企微回调 URL 并返回 echostr 明文。
@@ -99,24 +98,16 @@ func (s *CallbackService) Receive(ctx context.Context, applicationID uint, signa
})
}
// callbackCrypto 每次数据库密文解出当前回调凭据,使凭据轮换无需重启进程。
// callbackCrypto 每次读取数据库当前回调凭据,使凭据更新无需重启进程。
func (s *CallbackService) callbackCrypto(ctx context.Context, applicationID uint) (*CallbackCrypto, error) {
if s == nil || s.applications == nil || s.cipher == nil || applicationID == 0 {
if s == nil || s.applications == nil || applicationID == 0 {
return nil, errors.New(errors.CodeServiceUnavailable, "企业微信审批回调服务未配置")
}
application, err := s.applications.GetEnabled(ctx, applicationID)
if err != nil {
return nil, err
}
token, err := s.cipher.Decrypt(application.CallbackTokenCiphertext)
if err != nil {
return nil, err
}
aesKey, err := s.cipher.Decrypt(application.EncodingAESKeyCiphertext)
if err != nil {
return nil, err
}
return NewCallbackCrypto(token, aesKey, application.CorpID)
return NewCallbackCrypto(application.CallbackToken, application.EncodingAESKey, application.CorpID)
}
func applicationCallbackIdempotencyKey(applicationID uint, signature string) string {

View File

@@ -1,61 +0,0 @@
// Package wecom 提供企业微信外部系统 Adapter。
package wecom
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"io"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
var credentialAAD = []byte("junhong-wecom-credential-v1")
// CredentialCipher 使用 AES-256-GCM 加解密企业微信敏感凭据。
type CredentialCipher struct {
aead cipher.AEAD
}
// NewCredentialCipher 从 32 字节 Base64 密钥创建凭据加密器。
func NewCredentialCipher(encodedKey string) (*CredentialCipher, error) {
key, err := base64.StdEncoding.DecodeString(encodedKey)
if err != nil || len(key) != 32 {
return nil, errors.New(errors.CodeWeComCredentialInvalid, "企业微信凭据加密密钥必须是 32 字节 Base64")
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, errors.Wrap(errors.CodeWeComCredentialInvalid, err, "初始化企业微信凭据加密器失败")
}
aead, err := cipher.NewGCM(block)
if err != nil {
return nil, errors.Wrap(errors.CodeWeComCredentialInvalid, err, "初始化企业微信凭据加密模式失败")
}
return &CredentialCipher{aead: aead}, nil
}
// Encrypt 加密单个敏感凭据,返回 nonce 与密文组合。
func (c *CredentialCipher) Encrypt(plaintext string) ([]byte, error) {
if c == nil || c.aead == nil || plaintext == "" {
return nil, errors.New(errors.CodeWeComCredentialInvalid)
}
nonce := make([]byte, c.aead.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "生成企业微信凭据随机数失败")
}
return c.aead.Seal(nonce, nonce, []byte(plaintext), credentialAAD), nil
}
// Decrypt 解密单个企业微信敏感凭据。
func (c *CredentialCipher) Decrypt(ciphertext []byte) (string, error) {
if c == nil || c.aead == nil || len(ciphertext) <= c.aead.NonceSize() {
return "", errors.New(errors.CodeWeComCredentialInvalid)
}
nonce := ciphertext[:c.aead.NonceSize()]
plaintext, err := c.aead.Open(nil, nonce, ciphertext[c.aead.NonceSize():], credentialAAD)
if err != nil {
return "", errors.Wrap(errors.CodeWeComCredentialInvalid, err, "解密企业微信凭据失败")
}
return string(plaintext), nil
}

View File

@@ -34,11 +34,6 @@ type TokenApplicationRepository interface {
MarkConnected(ctx context.Context, applicationID uint, connectedAt time.Time) error
}
// TokenCredentialCipher 定义 access_token Provider 所需的凭据解密边界。
type TokenCredentialCipher interface {
Decrypt(ciphertext []byte) (string, error)
}
// TokenIntegrationLog 定义企微外呼的 Integration Log 边界。
type TokenIntegrationLog interface {
Start(ctx context.Context, input integrationlog.Attempt) (*model.IntegrationLog, error)
@@ -48,7 +43,6 @@ type TokenIntegrationLog interface {
// TokenProvider 按应用缓存企业微信 access_token并用 Redis 短锁抑制并发回源。
type TokenProvider struct {
repo TokenApplicationRepository
cipher TokenCredentialCipher
redis *redis.Client
integration TokenIntegrationLog
httpClient *http.Client
@@ -60,7 +54,6 @@ type TokenProvider struct {
// NewTokenProvider 创建企业微信 access_token Provider。
func NewTokenProvider(
repo TokenApplicationRepository,
cipher TokenCredentialCipher,
redisClient *redis.Client,
integration TokenIntegrationLog,
baseURL string,
@@ -71,7 +64,7 @@ func NewTokenProvider(
timeout = constants.WeComDefaultHTTPTimeout
}
return &TokenProvider{
repo: repo, cipher: cipher, redis: redisClient, integration: integration,
repo: repo, redis: redisClient, integration: integration,
httpClient: &http.Client{Timeout: timeout}, baseURL: strings.TrimRight(baseURL, "/"),
logger: logger, now: time.Now,
}
@@ -79,7 +72,7 @@ func NewTokenProvider(
// GetAccessToken 优先读取应用缓存,未命中时串行调用企业微信 Token 接口。
func (p *TokenProvider) GetAccessToken(ctx context.Context, applicationID uint) (string, error) {
if p == nil || p.repo == nil || p.cipher == nil || p.integration == nil || p.httpClient == nil || applicationID == 0 {
if p == nil || p.repo == nil || p.integration == nil || p.httpClient == nil || applicationID == 0 {
return "", errors.New(errors.CodeWeComCredentialInvalid)
}
cacheKey := constants.RedisWeComAccessTokenKey(applicationID)
@@ -162,11 +155,7 @@ func (p *TokenProvider) fetchAndCache(ctx context.Context, applicationID uint, c
if err != nil {
return "", err
}
secret, err := p.cipher.Decrypt(application.SecretCiphertext)
if err != nil {
return "", err
}
request, err := p.newTokenRequest(ctx, application.CorpID, secret)
request, err := p.newTokenRequest(ctx, application.CorpID, application.Secret)
if err != nil {
return "", err
}

View File

@@ -7,9 +7,9 @@ type SaveWeComApplicationRequest struct {
CorpID string `json:"corp_id" validate:"required,min=1,max=64" description:"企业微信企业 ID"`
AgentID int64 `json:"agent_id" validate:"required,gt=0" description:"企业微信自建应用 AgentID"`
Name string `json:"name" validate:"required,min=1,max=100" description:"应用展示名称"`
Secret string `json:"secret" validate:"required,min=1,max=512" description:"应用 Secret管理端使用明文传入,服务端加密保存"`
CallbackToken string `json:"callback_token" validate:"required,min=1,max=512" description:"回调 Token管理端使用明文传入,服务端加密保存"`
EncodingAESKey string `json:"encoding_aes_key" validate:"required,len=43" description:"回调 EncodingAESKey管理端使用明文传入,服务端加密保存"`
Secret string `json:"secret" validate:"required,min=1,max=512" description:"应用 Secret服务端明文保存"`
CallbackToken string `json:"callback_token" validate:"required,min=1,max=512" description:"回调 Token服务端明文保存"`
EncodingAESKey string `json:"encoding_aes_key" validate:"required,len=43" description:"回调 EncodingAESKey服务端明文保存"`
Status int `json:"status" validate:"oneof=0 1" enum:"0,1" description:"状态 (0:禁用, 1:启用)"`
}

View File

@@ -6,21 +6,21 @@ import (
"gorm.io/gorm"
)
// WeComApplication 企业微信自建应用及加密连接凭据。
// WeComApplication 企业微信自建应用及连接凭据。
type WeComApplication struct {
gorm.Model
CorpID string `gorm:"column:corp_id;type:varchar(64);not null;uniqueIndex:uq_wecom_application_identity,priority:1,where:deleted_at IS NULL" json:"corp_id"`
AgentID int64 `gorm:"column:agent_id;type:bigint;not null;uniqueIndex:uq_wecom_application_identity,priority:2,where:deleted_at IS NULL" json:"agent_id"`
Name string `gorm:"column:name;type:varchar(100);not null" json:"name"`
SecretCiphertext []byte `gorm:"column:secret_ciphertext;type:bytea;not null" json:"-"`
CallbackTokenCiphertext []byte `gorm:"column:callback_token_ciphertext;type:bytea;not null" json:"-"`
EncodingAESKeyCiphertext []byte `gorm:"column:encoding_aes_key_ciphertext;type:bytea;not null" json:"-"`
DefaultCreatorUserID string `gorm:"column:default_creator_userid;type:varchar(64);not null;default:''" json:"default_creator_userid"`
DefaultCreatorName string `gorm:"column:default_creator_name;type:varchar(100);not null;default:''" json:"default_creator_name"`
Status int `gorm:"column:status;type:int;not null;default:1" json:"status"`
CreatedBy uint `gorm:"column:created_by;not null" json:"created_by"`
UpdatedBy uint `gorm:"column:updated_by;not null" json:"updated_by"`
LastConnectedAt *time.Time `gorm:"column:last_connected_at;type:timestamptz" json:"last_connected_at,omitempty"`
CorpID string `gorm:"column:corp_id;type:varchar(64);not null;uniqueIndex:uq_wecom_application_identity,priority:1,where:deleted_at IS NULL" json:"corp_id"`
AgentID int64 `gorm:"column:agent_id;type:bigint;not null;uniqueIndex:uq_wecom_application_identity,priority:2,where:deleted_at IS NULL" json:"agent_id"`
Name string `gorm:"column:name;type:varchar(100);not null" json:"name"`
Secret string `gorm:"column:secret;type:varchar(512);not null" json:"-"`
CallbackToken string `gorm:"column:callback_token;type:varchar(512);not null" json:"-"`
EncodingAESKey string `gorm:"column:encoding_aes_key;type:varchar(43);not null" json:"-"`
DefaultCreatorUserID string `gorm:"column:default_creator_userid;type:varchar(64);not null;default:''" json:"default_creator_userid"`
DefaultCreatorName string `gorm:"column:default_creator_name;type:varchar(100);not null;default:''" json:"default_creator_name"`
Status int `gorm:"column:status;type:int;not null;default:1" json:"status"`
CreatedBy uint `gorm:"column:created_by;not null" json:"created_by"`
UpdatedBy uint `gorm:"column:updated_by;not null" json:"updated_by"`
LastConnectedAt *time.Time `gorm:"column:last_connected_at;type:timestamptz" json:"last_connected_at,omitempty"`
}
// TableName 指定企业微信应用表名。