feat: 停复机新规则转正,删除总开关与旧口径
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m35s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m35s
- 删除总开关 stop_resume.unified_rule_enabled(配置、加载、部署工作流与 docker-compose.prod.yml 环境变量) - 复机不看停机原因:删除只对轮询停因复机的旧分支与 ListByDeviceIDAndPollingStopReasons;设备复机取全部停机卡 - 实名门槛只按卡所属通道实名管控判断,删除行业卡与卡/设备实名策略放行;暂停轮询的卡不自动停复机无条件生效 - 通道阈值新周期解锁后的复机条件固定为新规则复机条件 - 资产自动续费复机同步新规则:不看停因、暂停轮询不复机、设备取全部停机卡、复机成功清除停因 - 实名轮询不再按行业卡跳过;所属运营商账户未开启实名管控时跳过查询,但卡未实名且所在资产有等待实名激活的套餐时照常查询,避免囤货套餐无法激活
This commit is contained in:
@@ -63,8 +63,6 @@ jobs:
|
|||||||
# 卡流量日快照计量灰度(rebuild-card-traffic-metering-and-stop-resume):仅测试设备 10 走新算法
|
# 卡流量日快照计量灰度(rebuild-card-traffic-metering-and-stop-resume):仅测试设备 10 走新算法
|
||||||
printf 'JUNHONG_TRAFFIC_CALC_MODE=gray\n'
|
printf 'JUNHONG_TRAFFIC_CALC_MODE=gray\n'
|
||||||
printf 'JUNHONG_TRAFFIC_CALC_CARDS=device:10\n'
|
printf 'JUNHONG_TRAFFIC_CALC_CARDS=device:10\n'
|
||||||
# 停复机新规则:复机不看停机原因、实名按通道实名管控判断、暂停轮询的卡不自动停复机
|
|
||||||
printf 'JUNHONG_STOP_RESUME_UNIFIED_RULE_ENABLED=true\n'
|
|
||||||
} > ${{ env.DEPLOY_DIR }}/.env
|
} > ${{ env.DEPLOY_DIR }}/.env
|
||||||
|
|
||||||
# 强制更新 docker-compose.prod.yml(确保使用最新配置)
|
# 强制更新 docker-compose.prod.yml(确保使用最新配置)
|
||||||
|
|||||||
@@ -77,11 +77,10 @@ services:
|
|||||||
- JUNHONG_WECOM_TIMEOUT=10s
|
- JUNHONG_WECOM_TIMEOUT=10s
|
||||||
- JUNHONG_APPROVAL_LEGACY_REFUND_MANUAL_ENABLED=${JUNHONG_APPROVAL_LEGACY_REFUND_MANUAL_ENABLED:-true}
|
- JUNHONG_APPROVAL_LEGACY_REFUND_MANUAL_ENABLED=${JUNHONG_APPROVAL_LEGACY_REFUND_MANUAL_ENABLED:-true}
|
||||||
- JUNHONG_APPROVAL_LEGACY_OFFLINE_RECHARGE_PAY_ENABLED=${JUNHONG_APPROVAL_LEGACY_OFFLINE_RECHARGE_PAY_ENABLED:-true}
|
- JUNHONG_APPROVAL_LEGACY_OFFLINE_RECHARGE_PAY_ENABLED=${JUNHONG_APPROVAL_LEGACY_OFFLINE_RECHARGE_PAY_ENABLED:-true}
|
||||||
# 卡流量日快照计量与停复机新规则(API 与 Worker 必须一致;默认与未配置时行为不变)
|
# 卡流量日快照计量(API 与 Worker 必须一致;默认与未配置时行为不变)
|
||||||
- JUNHONG_TRAFFIC_CALC_MODE=${JUNHONG_TRAFFIC_CALC_MODE:-off}
|
- JUNHONG_TRAFFIC_CALC_MODE=${JUNHONG_TRAFFIC_CALC_MODE:-off}
|
||||||
- JUNHONG_TRAFFIC_CALC_CARDS=${JUNHONG_TRAFFIC_CALC_CARDS:-}
|
- JUNHONG_TRAFFIC_CALC_CARDS=${JUNHONG_TRAFFIC_CALC_CARDS:-}
|
||||||
- JUNHONG_TRAFFIC_FLOW_SNAPSHOT_ENABLED=${JUNHONG_TRAFFIC_FLOW_SNAPSHOT_ENABLED:-true}
|
- JUNHONG_TRAFFIC_FLOW_SNAPSHOT_ENABLED=${JUNHONG_TRAFFIC_FLOW_SNAPSHOT_ENABLED:-true}
|
||||||
- JUNHONG_STOP_RESUME_UNIFIED_RULE_ENABLED=${JUNHONG_STOP_RESUME_UNIFIED_RULE_ENABLED:-false}
|
|
||||||
# 短信服务配置
|
# 短信服务配置
|
||||||
- JUNHONG_SMS_GATEWAY_URL=https://gateway.sms.whjhft.com:8443
|
- JUNHONG_SMS_GATEWAY_URL=https://gateway.sms.whjhft.com:8443
|
||||||
- JUNHONG_SMS_USERNAME=JH0001
|
- JUNHONG_SMS_USERNAME=JH0001
|
||||||
@@ -148,11 +147,10 @@ services:
|
|||||||
# 七月迭代:单 Worker 测试环境同时承担调度和消费
|
# 七月迭代:单 Worker 测试环境同时承担调度和消费
|
||||||
- JUNHONG_WORKER_ROLE=${JUNHONG_WORKER_ROLE:-all}
|
- JUNHONG_WORKER_ROLE=${JUNHONG_WORKER_ROLE:-all}
|
||||||
- JUNHONG_WORKER_INSTANCE_NAME=${JUNHONG_WORKER_INSTANCE_NAME:-test-worker-all-1}
|
- JUNHONG_WORKER_INSTANCE_NAME=${JUNHONG_WORKER_INSTANCE_NAME:-test-worker-all-1}
|
||||||
# 卡流量日快照计量与停复机新规则(API 与 Worker 必须一致;默认与未配置时行为不变)
|
# 卡流量日快照计量(API 与 Worker 必须一致;默认与未配置时行为不变)
|
||||||
- JUNHONG_TRAFFIC_CALC_MODE=${JUNHONG_TRAFFIC_CALC_MODE:-off}
|
- JUNHONG_TRAFFIC_CALC_MODE=${JUNHONG_TRAFFIC_CALC_MODE:-off}
|
||||||
- JUNHONG_TRAFFIC_CALC_CARDS=${JUNHONG_TRAFFIC_CALC_CARDS:-}
|
- JUNHONG_TRAFFIC_CALC_CARDS=${JUNHONG_TRAFFIC_CALC_CARDS:-}
|
||||||
- JUNHONG_TRAFFIC_FLOW_SNAPSHOT_ENABLED=${JUNHONG_TRAFFIC_FLOW_SNAPSHOT_ENABLED:-true}
|
- JUNHONG_TRAFFIC_FLOW_SNAPSHOT_ENABLED=${JUNHONG_TRAFFIC_FLOW_SNAPSHOT_ENABLED:-true}
|
||||||
- JUNHONG_STOP_RESUME_UNIFIED_RULE_ENABLED=${JUNHONG_STOP_RESUME_UNIFIED_RULE_ENABLED:-false}
|
|
||||||
# Worker 执行企微审批提交、同步和恢复任务
|
# Worker 执行企微审批提交、同步和恢复任务
|
||||||
- JUNHONG_WECOM_BASE_URL=https://qyapi.weixin.qq.com
|
- JUNHONG_WECOM_BASE_URL=https://qyapi.weixin.qq.com
|
||||||
- JUNHONG_WECOM_TIMEOUT=10s
|
- JUNHONG_WECOM_TIMEOUT=10s
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ func (s *StopResumeService) QueryAutoRenewalResumeState(ctx context.Context, ass
|
|||||||
}
|
}
|
||||||
|
|
||||||
// autoRenewalResumeCards 解析自动续费资产对应的待复机卡:卡资产取自身,设备资产取该设备下
|
// autoRenewalResumeCards 解析自动续费资产对应的待复机卡:卡资产取自身,设备资产取该设备下
|
||||||
// 因轮询原因停机的绑定卡(复用既有设备卡查询,不另立绑定关系口径)。
|
// 全部停机的绑定卡(不看停机原因,复用既有设备卡查询,不另立绑定关系口径)。
|
||||||
func (s *StopResumeService) autoRenewalResumeCards(ctx context.Context, assetType string, assetID uint) ([]*model.IotCard, error) {
|
func (s *StopResumeService) autoRenewalResumeCards(ctx context.Context, assetType string, assetID uint) ([]*model.IotCard, error) {
|
||||||
if assetID == 0 {
|
if assetID == 0 {
|
||||||
return nil, errors.New(errors.CodeInvalidParam, "自动续费复机资产无效")
|
return nil, errors.New(errors.CodeInvalidParam, "自动续费复机资产无效")
|
||||||
@@ -179,7 +179,7 @@ func (s *StopResumeService) autoRenewalResumeCards(ctx context.Context, assetTyp
|
|||||||
}
|
}
|
||||||
return []*model.IotCard{card}, nil
|
return []*model.IotCard{card}, nil
|
||||||
case constants.AssetWalletResourceTypeDevice:
|
case constants.AssetWalletResourceTypeDevice:
|
||||||
cards, err := s.iotCardStore.ListByDeviceIDAndPollingStopReasons(ctx, assetID)
|
cards, err := s.iotCardStore.ListByDeviceIDAndNetworkStatus(ctx, assetID, constants.NetworkStatusOffline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备待复机卡失败")
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备待复机卡失败")
|
||||||
}
|
}
|
||||||
@@ -197,11 +197,13 @@ func (s *StopResumeService) autoRenewalCardReady(ctx context.Context, card *mode
|
|||||||
if card.NetworkStatus == constants.NetworkStatusOnline {
|
if card.NetworkStatus == constants.NetworkStatusOnline {
|
||||||
return false, "卡当前已在线,无需复机", nil
|
return false, "卡当前已在线,无需复机", nil
|
||||||
}
|
}
|
||||||
if isRiskGatewayExtend(card.GatewayExtend) {
|
// 与停复机规则一致:不看停机原因;卡或所在设备暂停轮询的不自动复机(风险停机与已销户的卡已关闭轮询)。
|
||||||
return false, "网关扩展状态为风险停机或已销户,不自动复机", nil
|
suspended, err := s.autoControlSuspended(ctx, card)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err
|
||||||
}
|
}
|
||||||
if !isPollingStopReason(card.StopReason) {
|
if suspended {
|
||||||
return false, "停因不属于可轮询复机范围,不自动复机", nil
|
return false, "卡或所在设备已暂停轮询,不自动复机", nil
|
||||||
}
|
}
|
||||||
blocked, err := s.channelThresholdBlocked(ctx, card)
|
blocked, err := s.channelThresholdBlocked(ctx, card)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -266,10 +268,8 @@ func (s *StopResumeService) autoRenewalResumeCard(ctx context.Context, card *mod
|
|||||||
"network_status": constants.NetworkStatusOnline,
|
"network_status": constants.NetworkStatusOnline,
|
||||||
"resumed_at": time.Now(),
|
"resumed_at": time.Now(),
|
||||||
}
|
}
|
||||||
// 判定已限定停因属于可轮询复机范围,复机成功后清除停因,与既有轮询复机口径一致。
|
// 复机成功后清除停因,与轮询复机口径一致。
|
||||||
if isPollingStopReason(card.StopReason) {
|
fields["stop_reason"] = ""
|
||||||
fields["stop_reason"] = ""
|
|
||||||
}
|
|
||||||
if err := s.updateCardAndAppendNetworkSeries(ctx, card, fields,
|
if err := s.updateCardAndAppendNetworkSeries(ctx, card, fields,
|
||||||
constants.CardObservationSceneBusinessResume, "online", uuid.NewString(), actionCode, summary, attempt.log.IntegrationID); err != nil {
|
constants.CardObservationSceneBusinessResume, "online", uuid.NewString(), actionCode, summary, attempt.log.IntegrationID); err != nil {
|
||||||
if logErr := s.completeCardCommandAttempt(ctx, attempt, nil, false); logErr != nil {
|
if logErr := s.completeCardCommandAttempt(ctx, attempt, nil, false); logErr != nil {
|
||||||
|
|||||||
@@ -100,30 +100,20 @@ func (s *StopResumeService) ResumeCardForChannelThreshold(ctx context.Context, c
|
|||||||
|
|
||||||
// ChannelThresholdResumeReady 判断通道阈值跨期解锁后的卡是否满足自动复机条件。
|
// ChannelThresholdResumeReady 判断通道阈值跨期解锁后的卡是否满足自动复机条件。
|
||||||
//
|
//
|
||||||
// 条件全部复用既有单一事实源:有效主套餐、流量未耗尽、实名满足、非风险网关扩展,且不存在其他停机锁
|
// 复机条件固定为停复机规则的复机条件:未暂停轮询、有效主套餐、流量未耗尽、实名满足;不看停机原因与网关扩展
|
||||||
// (停因为空或正是通道阈值本身)。返回的 reason 是不满足原因(可安全记录),满足时为空。
|
// (风险停机与已销户的卡已关闭轮询)。返回的 reason 是不满足原因(可安全记录),满足时为空。
|
||||||
func (s *StopResumeService) ChannelThresholdResumeReady(ctx context.Context, cardID uint) (bool, string, error) {
|
func (s *StopResumeService) ChannelThresholdResumeReady(ctx context.Context, cardID uint) (bool, string, error) {
|
||||||
card, err := s.iotCardStore.GetByID(ctx, cardID)
|
card, err := s.iotCardStore.GetByID(ctx, cardID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", errors.Wrap(errors.CodeDatabaseError, err, "读取通道阈值复机判定卡事实失败")
|
return false, "", errors.Wrap(errors.CodeDatabaseError, err, "读取通道阈值复机判定卡事实失败")
|
||||||
}
|
}
|
||||||
if unifiedStopResumeEnabled() {
|
// 卡或所在设备暂停轮询时只解锁不复机。
|
||||||
// 总开关开启时不看其他停因与网关扩展状态,卡或所在设备暂停轮询时只解锁不复机。
|
suspended, suspendErr := s.autoControlSuspended(ctx, card)
|
||||||
suspended, suspendErr := s.autoControlSuspended(ctx, card)
|
if suspendErr != nil {
|
||||||
if suspendErr != nil {
|
return false, "", suspendErr
|
||||||
return false, "", suspendErr
|
}
|
||||||
}
|
if suspended {
|
||||||
if suspended {
|
return false, "卡或所在设备已暂停轮询,不自动复机", nil
|
||||||
return false, "卡或所在设备已暂停轮询,不自动复机", nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 其他停机锁:风险网关扩展(风险停机/销户)或非通道阈值的停因。
|
|
||||||
if isRiskGatewayExtend(card.GatewayExtend) {
|
|
||||||
return false, "网关扩展状态为风险停机或已销户,不自动复机", nil
|
|
||||||
}
|
|
||||||
if card.StopReason != "" && card.StopReason != constants.StopReasonChannelThreshold {
|
|
||||||
return false, "存在其他停机原因,不自动复机", nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
hasPackage, err := s.hasValidPackage(ctx, card)
|
hasPackage, err := s.hasValidPackage(ctx, card)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -172,11 +172,7 @@ func (s *StopResumeService) EvaluateAndAct(ctx context.Context, card *model.IotC
|
|||||||
return stopErr
|
return stopErr
|
||||||
|
|
||||||
case constants.NetworkStatusOffline:
|
case constants.NetworkStatusOffline:
|
||||||
// 卡停机,检查是否可以复机
|
// 卡停机,检查是否可以复机:不看停机原因,是否复机只由本系统规则决定
|
||||||
// 总开关关闭时只处理轮询系统引起的停机原因(手动停机等不自动复机);开启时不看停机原因
|
|
||||||
if !autoResumableStopReason(card.StopReason) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
shouldResume, err := s.shouldResume(ctx, card)
|
shouldResume, err := s.shouldResume(ctx, card)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -208,17 +204,6 @@ func isRiskGatewayExtend(extend string) bool {
|
|||||||
trimmed == constants.GatewayCardExtendCancelled
|
trimmed == constants.GatewayCardExtendCancelled
|
||||||
}
|
}
|
||||||
|
|
||||||
// isPollingStopReason 判断停机原因是否为轮询系统引起(可自动复机)
|
|
||||||
func isPollingStopReason(reason string) bool {
|
|
||||||
switch reason {
|
|
||||||
case constants.StopReasonTrafficExhausted,
|
|
||||||
constants.StopReasonNoPackage,
|
|
||||||
constants.StopReasonNotRealname:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// containsStopReason 判断停机原因集合是否包含指定原因。
|
// containsStopReason 判断停机原因集合是否包含指定原因。
|
||||||
func containsStopReason(reasons []string, target string) bool {
|
func containsStopReason(reasons []string, target string) bool {
|
||||||
for _, reason := range reasons {
|
for _, reason := range reasons {
|
||||||
@@ -346,32 +331,6 @@ func (s *StopResumeService) isTrafficExhausted(ctx context.Context, card *model.
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isRealnameOK 检查卡是否满足实名要求
|
|
||||||
// 行业卡、已实名卡直接放行;其他卡按有效设备或自身的无需实名策略判断。
|
|
||||||
func (s *StopResumeService) isRealnameOK(ctx context.Context, card *model.IotCard) (bool, error) {
|
|
||||||
// 总开关开启时实名只按卡所属通道的实名管控判断,不区分行业卡、不看卡或设备实名策略。
|
|
||||||
if unifiedStopResumeEnabled() {
|
|
||||||
return s.unifiedRealnameOK(ctx, card)
|
|
||||||
}
|
|
||||||
if card.CardCategory == constants.CardCategoryIndustry || card.RealNameStatus == constants.RealNameStatusVerified {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceID, bound, err := s.getCardDeviceID(ctx, card)
|
|
||||||
if err != nil {
|
|
||||||
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询卡实名策略绑定关系失败")
|
|
||||||
}
|
|
||||||
if !bound {
|
|
||||||
return card.RealnamePolicy == constants.RealnamePolicyNone, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var device model.Device
|
|
||||||
if err := s.db.WithContext(ctx).Select("realname_policy").First(&device, deviceID).Error; err != nil {
|
|
||||||
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询设备实名策略失败")
|
|
||||||
}
|
|
||||||
return device.RealnamePolicy == constants.RealnamePolicyNone, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkStopReasons 检查卡的停机原因列表,按优先级排序
|
// checkStopReasons 检查卡的停机原因列表,按优先级排序
|
||||||
// 优先级:no_package > traffic_exhausted > not_realname
|
// 优先级:no_package > traffic_exhausted > not_realname
|
||||||
func (s *StopResumeService) checkStopReasons(ctx context.Context, card *model.IotCard) ([]string, error) {
|
func (s *StopResumeService) checkStopReasons(ctx context.Context, card *model.IotCard) ([]string, error) {
|
||||||
@@ -492,14 +451,8 @@ func (s *StopResumeService) resumeDeviceCards(ctx context.Context, deviceID uint
|
|||||||
}
|
}
|
||||||
defer s.redis.Del(ctx, lockKey)
|
defer s.redis.Del(ctx, lockKey)
|
||||||
|
|
||||||
// 查询设备下待复机的卡:总开关关闭时只取因轮询原因停机的卡,开启时取全部停机卡(不看停机原因)
|
// 查询设备下全部停机卡(不看停机原因)
|
||||||
var cards []*model.IotCard
|
cards, err := s.iotCardStore.ListByDeviceIDAndNetworkStatus(ctx, deviceID, constants.NetworkStatusOffline)
|
||||||
var err error
|
|
||||||
if unifiedStopResumeEnabled() {
|
|
||||||
cards, err = s.iotCardStore.ListByDeviceIDAndNetworkStatus(ctx, deviceID, constants.NetworkStatusOffline)
|
|
||||||
} else {
|
|
||||||
cards, err = s.iotCardStore.ListByDeviceIDAndPollingStopReasons(ctx, deviceID)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("查询设备停机卡失败",
|
s.logger.Error("查询设备停机卡失败",
|
||||||
zap.Uint("device_id", deviceID), zap.Error(err))
|
zap.Uint("device_id", deviceID), zap.Error(err))
|
||||||
@@ -597,14 +550,6 @@ func (s *StopResumeService) resumeSingleCard(ctx context.Context, cardID uint) e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 总开关关闭时只处理轮询系统引起的停机;开启时不看停机原因
|
|
||||||
if !autoResumableStopReason(card.StopReason) {
|
|
||||||
s.logger.Debug("卡非轮询停机原因,不自动复机",
|
|
||||||
zap.Uint("card_id", cardID),
|
|
||||||
zap.String("stop_reason", card.StopReason))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查复机条件
|
// 检查复机条件
|
||||||
resume, err := s.shouldResume(ctx, card)
|
resume, err := s.shouldResume(ctx, card)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||||
"github.com/break/junhong_cmp_fiber/pkg/config"
|
|
||||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||||
)
|
)
|
||||||
@@ -15,25 +14,12 @@ import (
|
|||||||
// realnameControlEnabled 是通道实名管控开启时的取值。
|
// realnameControlEnabled 是通道实名管控开启时的取值。
|
||||||
const realnameControlEnabled = 1
|
const realnameControlEnabled = 1
|
||||||
|
|
||||||
// unifiedStopResumeEnabled 判定停复机新规则总开关是否开启。
|
// 停复机规则(已转正为唯一口径):复机不看停机原因;实名按卡所属通道的实名管控判断
|
||||||
//
|
// (不区分行业卡、不看卡或设备实名策略);卡或所在设备暂停轮询时不自动停复机。
|
||||||
// 开启后:复机不看停机原因;实名按卡所属通道的实名管控判断(不区分行业卡、不看卡/设备实名策略);
|
|
||||||
// 卡或所在设备暂停轮询时不自动停复机。关闭时全部停复机路径与原有行为一致。
|
|
||||||
func unifiedStopResumeEnabled() bool {
|
|
||||||
cfg := config.Get()
|
|
||||||
return cfg != nil && cfg.StopResume.UnifiedRuleEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
// autoResumableStopReason 判定该停因的停机卡能否进入自动复机判断:总开关开启时不看停因,
|
// autoControlSuspended 判定该卡是否因暂停轮询而不自动停复机:卡自身或其有效绑定设备暂停轮询。
|
||||||
// 关闭时只处理轮询系统引起的停机。
|
|
||||||
func autoResumableStopReason(reason string) bool {
|
|
||||||
return unifiedStopResumeEnabled() || isPollingStopReason(reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
// autoControlSuspended 判定总开关开启时该卡是否因暂停轮询而不自动停复机:卡自身或其有效绑定设备暂停轮询。
|
|
||||||
// 总开关关闭时恒为 false。
|
|
||||||
func (s *StopResumeService) autoControlSuspended(ctx context.Context, card *model.IotCard) (bool, error) {
|
func (s *StopResumeService) autoControlSuspended(ctx context.Context, card *model.IotCard) (bool, error) {
|
||||||
if !unifiedStopResumeEnabled() || card == nil {
|
if card == nil {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
if !card.EnablePolling {
|
if !card.EnablePolling {
|
||||||
@@ -69,8 +55,9 @@ func (s *StopResumeService) channelRealnameControlled(ctx context.Context, card
|
|||||||
return values[0] == realnameControlEnabled, nil
|
return values[0] == realnameControlEnabled, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// unifiedRealnameOK 是总开关开启后的实名门槛:通道未开启实名管控即满足;开启时只看卡是否已实名。
|
// isRealnameOK 是停复机的实名门槛:通道未开启实名管控即满足;开启时只看卡是否已实名。
|
||||||
func (s *StopResumeService) unifiedRealnameOK(ctx context.Context, card *model.IotCard) (bool, error) {
|
// 查询通道失败时返回错误,调用方本次不对该卡发起停复机。
|
||||||
|
func (s *StopResumeService) isRealnameOK(ctx context.Context, card *model.IotCard) (bool, error) {
|
||||||
controlled, err := s.channelRealnameControlled(ctx, card)
|
controlled, err := s.channelRealnameControlled(ctx, card)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
|||||||
@@ -1372,22 +1372,6 @@ func (s *IotCardStore) ListByDeviceIDAndNetworkStatus(ctx context.Context, devic
|
|||||||
return cards, err
|
return cards, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListByDeviceIDAndPollingStopReasons 查询设备下因轮询原因停机的所有卡
|
|
||||||
// 用于 resumeDeviceCards:只复机由轮询系统停机的卡(不干预手动停机)
|
|
||||||
func (s *IotCardStore) ListByDeviceIDAndPollingStopReasons(ctx context.Context, deviceID uint) ([]*model.IotCard, error) {
|
|
||||||
pollingReasons := []string{
|
|
||||||
constants.StopReasonTrafficExhausted,
|
|
||||||
constants.StopReasonNoPackage,
|
|
||||||
constants.StopReasonNotRealname,
|
|
||||||
}
|
|
||||||
var cards []*model.IotCard
|
|
||||||
err := s.db.WithContext(ctx).
|
|
||||||
Where("id IN (SELECT iot_card_id FROM tb_device_sim_binding WHERE device_id = ? AND bind_status = 1 AND deleted_at IS NULL)", deviceID).
|
|
||||||
Where("network_status = 0 AND stop_reason IN ? AND deleted_at IS NULL", pollingReasons).
|
|
||||||
Find(&cards).Error
|
|
||||||
return cards, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateStopReason 更新卡的停机原因字段
|
// UpdateStopReason 更新卡的停机原因字段
|
||||||
func (s *IotCardStore) UpdateStopReason(ctx context.Context, cardID uint, reason string) error {
|
func (s *IotCardStore) UpdateStopReason(ctx context.Context, cardID uint, reason string) error {
|
||||||
return s.db.WithContext(ctx).
|
return s.db.WithContext(ctx).
|
||||||
|
|||||||
@@ -67,8 +67,19 @@ func (h *PollingRealnameHandler) Handle(ctx context.Context, task *asynq.Task) e
|
|||||||
if !h.base.priorityInPollingScope(ctx, priority, card) {
|
if !h.base.priorityInPollingScope(ctx, priority, card) {
|
||||||
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingRealname)
|
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingRealname)
|
||||||
}
|
}
|
||||||
if card.CardCategory == constants.CardCategoryIndustry || h.gateway == nil {
|
if h.gateway == nil {
|
||||||
priority.FailFinal(ctx, "卡类型不适用实名轮询或实名查询能力未配置")
|
priority.FailFinal(ctx, "实名查询能力未配置")
|
||||||
|
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingRealname)
|
||||||
|
}
|
||||||
|
// 所属运营商账户未开启实名管控时实名结果不影响停复机,跳过查询并按正常间隔重入队(不再按行业卡跳过)。
|
||||||
|
skippable, scopeErr := h.base.realnameQuerySkippable(ctx, card)
|
||||||
|
if scopeErr != nil {
|
||||||
|
priority.FailRetryable(ctx, "查询运营商账户实名管控失败")
|
||||||
|
return h.failAndRequeue(ctx, cardID, startedAt, "查询运营商账户实名管控失败", scopeErr)
|
||||||
|
}
|
||||||
|
if skippable {
|
||||||
|
priority.FailFinal(ctx, "所属运营商账户未开启实名管控,跳过实名查询")
|
||||||
|
h.base.logger.Debug("所属运营商账户未开启实名管控,跳过实名查询", zap.Uint("card_id", cardID), zap.Uint("carrier_id", card.CarrierID))
|
||||||
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingRealname)
|
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingRealname)
|
||||||
}
|
}
|
||||||
priority.MarkExecution(ctx)
|
priority.MarkExecution(ctx)
|
||||||
|
|||||||
94
internal/task/polling_realname_scope.go
Normal file
94
internal/task/polling_realname_scope.go
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package task
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||||
|
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||||
|
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// realnameControlCacheTTL 是运营商账户实名管控在进程内的缓存时长;账户配置修改最迟在该时长后生效。
|
||||||
|
const realnameControlCacheTTL = time.Minute
|
||||||
|
|
||||||
|
type realnameControlEntry struct {
|
||||||
|
controlled bool
|
||||||
|
loadedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var realnameControlCache = struct {
|
||||||
|
sync.Mutex
|
||||||
|
values map[uint]realnameControlEntry
|
||||||
|
}{values: map[uint]realnameControlEntry{}}
|
||||||
|
|
||||||
|
// realnameQuerySkippable 判断本次实名轮询能否跳过查询:卡所属运营商账户未开启实名管控时实名结果不影响停复机,跳过;
|
||||||
|
// 但卡未实名且所在资产(绑定设备时按设备)有等待实名激活的套餐时照常查询,避免囤货套餐因发现不了实名而无法激活。
|
||||||
|
// 账户或套餐查询失败返回错误,调用方按可恢复失败处理。
|
||||||
|
func (b *PollingBase) realnameQuerySkippable(ctx context.Context, card *model.IotCard) (bool, error) {
|
||||||
|
controlled, err := b.carrierRealnameControlled(ctx, card.CarrierID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if controlled {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if card.RealNameStatus == constants.RealNameStatusVerified {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
pending, err := b.hasPendingRealnameActivation(ctx, card)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return !pending, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *PollingBase) carrierRealnameControlled(ctx context.Context, carrierID uint) (bool, error) {
|
||||||
|
realnameControlCache.Lock()
|
||||||
|
entry, ok := realnameControlCache.values[carrierID]
|
||||||
|
realnameControlCache.Unlock()
|
||||||
|
if ok && time.Since(entry.loadedAt) < realnameControlCacheTTL {
|
||||||
|
return entry.controlled, nil
|
||||||
|
}
|
||||||
|
if b.db == nil {
|
||||||
|
return false, errors.New(errors.CodeInternalError, "实名轮询数据库未配置")
|
||||||
|
}
|
||||||
|
var values []int
|
||||||
|
if err := b.db.WithContext(ctx).Model(&model.Carrier{}).Where("id = ?", carrierID).Limit(1).
|
||||||
|
Pluck("realname_control_enabled", &values).Error; err != nil {
|
||||||
|
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询运营商账户实名管控失败")
|
||||||
|
}
|
||||||
|
if len(values) == 0 {
|
||||||
|
return false, errors.New(errors.CodeNotFound, "卡所属运营商账户不存在,无法判断实名管控")
|
||||||
|
}
|
||||||
|
controlled := values[0] == 1
|
||||||
|
realnameControlCache.Lock()
|
||||||
|
realnameControlCache.values[carrierID] = realnameControlEntry{controlled: controlled, loadedAt: time.Now()}
|
||||||
|
realnameControlCache.Unlock()
|
||||||
|
return controlled, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasPendingRealnameActivation 判断卡所在资产是否有等待实名激活的待生效套餐。
|
||||||
|
func (b *PollingBase) hasPendingRealnameActivation(ctx context.Context, card *model.IotCard) (bool, error) {
|
||||||
|
query := b.db.WithContext(ctx).Model(&model.PackageUsage{}).
|
||||||
|
Where("pending_realname_activation = ? AND status = ?", true, constants.PackageUsageStatusPending)
|
||||||
|
var deviceIDs []uint
|
||||||
|
if !card.IsStandalone {
|
||||||
|
if err := b.db.WithContext(ctx).Model(&model.DeviceSimBinding{}).
|
||||||
|
Where("iot_card_id = ? AND bind_status = ?", card.ID, constants.BindStatusBound).
|
||||||
|
Limit(1).Pluck("device_id", &deviceIDs).Error; err != nil {
|
||||||
|
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询卡绑定设备失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(deviceIDs) > 0 {
|
||||||
|
query = query.Where("device_id = ?", deviceIDs[0])
|
||||||
|
} else {
|
||||||
|
query = query.Where("iot_card_id = ?", card.ID)
|
||||||
|
}
|
||||||
|
var count int64
|
||||||
|
if err := query.Limit(1).Count(&count).Error; err != nil {
|
||||||
|
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询待实名激活套餐失败")
|
||||||
|
}
|
||||||
|
return count > 0, nil
|
||||||
|
}
|
||||||
@@ -86,12 +86,16 @@
|
|||||||
|
|
||||||
### Requirement: 实名轮询按通道实名管控跳过
|
### Requirement: 实名轮询按通道实名管控跳过
|
||||||
|
|
||||||
系统 SHALL 在卡所属运营商账户未开启实名管控时跳过该卡的实名查询(结果不影响停复机),开启时照常查询;MUST NOT 再按卡类别(行业卡)跳过实名查询。
|
系统 SHALL 在卡所属运营商账户未开启实名管控时跳过该卡的实名查询(结果不影响停复机),开启时照常查询;MUST NOT 再按卡类别(行业卡)跳过实名查询。例外:卡未实名且所在资产(绑定设备时按设备)有等待实名激活的待生效套餐时,即使账户未开启实名管控也 MUST 照常查询,保证囤货套餐在卡实名后激活。查询账户实名管控失败时按可恢复失败处理并重入队。
|
||||||
|
|
||||||
#### Scenario: 未开管控账户的普通卡
|
#### Scenario: 未开管控账户的普通卡
|
||||||
- **WHEN** 卡所属账户 `realname_control_enabled = 0`,实名检查到期
|
- **WHEN** 卡所属账户 `realname_control_enabled = 0`,实名检查到期
|
||||||
- **THEN** 系统不调用网关查实名,按正常间隔安排下次检查
|
- **THEN** 系统不调用网关查实名,按正常间隔安排下次检查
|
||||||
|
|
||||||
|
#### Scenario: 未开管控账户中等待实名激活的卡
|
||||||
|
- **WHEN** 卡所属账户 `realname_control_enabled = 0`,卡未实名,所在设备有等待实名激活的待生效套餐
|
||||||
|
- **THEN** 系统照常调用网关查实名,卡实名后激活该套餐
|
||||||
|
|
||||||
#### Scenario: 开启管控账户的行业卡
|
#### Scenario: 开启管控账户的行业卡
|
||||||
- **WHEN** 卡所属账户开启实名管控,行业卡实名检查到期
|
- **WHEN** 卡所属账户开启实名管控,行业卡实名检查到期
|
||||||
- **THEN** 系统调用网关查实名
|
- **THEN** 系统调用网关查实名
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
## 2. 规则演练
|
## 2. 规则演练
|
||||||
|
|
||||||
- [x] 2.1 新增演练 Query:按批读取卡与资产数据,复用新规则判断函数(不受总开关影响,始终按新规则判断),支持假设的实名管控配置;只读。(纯判断函数 `internal/domain/stopresume`,读侧 `internal/query/stopresumerule` 按 1000 张一批读取绑定、设备轮询开关、套餐、通道实名管控与通道阈值锁;评估主流程仍按逐卡读取,第二次发布并入时改为复用同一判断函数)
|
- [x] 2.1 新增演练 Query:按批读取卡与资产数据,复用新规则判断函数(不受总开关影响,始终按新规则判断),支持假设的实名管控配置;只读。(纯判断函数 `internal/domain/stopresume`,读侧 `internal/query/stopresumerule` 按 1000 张一批读取绑定、设备轮询开关、套餐、通道实名管控与通道阈值锁;评估主流程仍按逐卡读取,与演练按同一套条件实现,未改为调用同一函数)
|
||||||
- [x] 2.2 `GET /stop-resume-rule/dry-run`(运营商账户、停机原因筛选,汇总 + 分页明细),仅超级管理员与平台用户;导出场景 `stop_resume_rule_dry_run`。(另加 `POST /stop-resume-rule/dry-run/export` 创建导出任务;明细可按 result 筛选,汇总不受影响)
|
- [x] 2.2 `GET /stop-resume-rule/dry-run`(运营商账户、停机原因筛选,汇总 + 分页明细),仅超级管理员与平台用户;导出场景 `stop_resume_rule_dry_run`。(另加 `POST /stop-resume-rule/dry-run/export` 创建导出任务;明细可按 result 筛选,汇总不受影响)
|
||||||
- [x] 2.3 同步路由与文档生成入口,重新生成接口文档。
|
- [x] 2.3 同步路由与文档生成入口,重新生成接口文档。
|
||||||
|
|
||||||
@@ -28,24 +28,24 @@
|
|||||||
|
|
||||||
## 5. 开启新规则(运营与维护者,第一次发布上线后)
|
## 5. 开启新规则(运营与维护者,第一次发布上线后)
|
||||||
|
|
||||||
- [ ] 5.1 为需要实名的运营商账户设置 `realname_control_enabled = 1`。
|
- [x] 5.1 为需要实名的运营商账户设置 `realname_control_enabled = 1`。(按用户 2026-10-25 决定"生产的都不用管",以测试环境为准:测试环境账户 1580、1583、1589 已开启实名管控)
|
||||||
- [ ] 5.2 生产执行演练并导出明细,运营确认"将被复机 / 将被停机"清单;需保持停机的卡先关闭轮询。
|
- [x] 5.2 生产执行演练并导出明细,运营确认"将被复机 / 将被停机"清单;需保持停机的卡先关闭轮询。(同上,测试环境已执行演练并核对,见 4.2)
|
||||||
- [ ] 5.3 维护者在生产开启 `JUNHONG_STOP_RESUME_UNIFIED_RULE_ENABLED` 并重启全部进程;首日监控复机成功率与终态错误数,确认稳定运行。
|
- [x] 5.3 维护者在生产开启 `JUNHONG_STOP_RESUME_UNIFIED_RULE_ENABLED` 并重启全部进程;首日监控复机成功率与终态错误数,确认稳定运行。(同上,测试环境部署配置已开启总开关并稳定运行;生产不处理)
|
||||||
|
|
||||||
## 第二次发布:转正(新规则在生产稳定运行后)
|
## 第二次发布:转正(新规则在生产稳定运行后)
|
||||||
|
|
||||||
## 6. 删除总开关与旧口径
|
## 6. 删除总开关与旧口径
|
||||||
|
|
||||||
- [ ] 6.1 `stop_resume_unified.go` 并入主逻辑:删除 `unifiedStopResumeEnabled` 与各分支,`autoResumableStopReason` 恒为真,`autoControlSuspended`、`unifiedRealnameOK` 无条件生效。
|
- [x] 6.1 `stop_resume_unified.go` 并入主逻辑:删除 `unifiedStopResumeEnabled` 与各分支,`autoResumableStopReason` 恒为真,`autoControlSuspended`、`unifiedRealnameOK` 无条件生效。(`stop_resume_unified.go` 只保留暂停轮询判断与按通道实名管控的 `isRealnameOK`)
|
||||||
- [ ] 6.2 `stop_resume_service.go` 删除旧口径:`isRealnameOK` 行业卡与 `realname_policy` 分支、`isPollingStopReason` 限定、`ListByDeviceIDAndPollingStopReasons` 的调用。
|
- [x] 6.2 `stop_resume_service.go` 删除旧口径:`isRealnameOK` 行业卡与 `realname_policy` 分支、`isPollingStopReason` 限定、`ListByDeviceIDAndPollingStopReasons` 的调用。(`isPollingStopReason` 与 `ListByDeviceIDAndPollingStopReasons` 已删除;资产自动续费复机同步改为不看停因、暂停轮询不复机、设备取全部停机卡)
|
||||||
- [ ] 6.3 删除 `pkg/config/config.go`、`loader.go`、`defaults/config.yaml` 的总开关,`.gitea/workflows/deploy.yaml` 与 `docker-compose.prod.yml` 中的 `JUNHONG_STOP_RESUME_UNIFIED_RULE_ENABLED`。
|
- [x] 6.3 删除 `pkg/config/config.go`、`loader.go`、`defaults/config.yaml` 的总开关,`.gitea/workflows/deploy.yaml` 与 `docker-compose.prod.yml` 中的 `JUNHONG_STOP_RESUME_UNIFIED_RULE_ENABLED`。
|
||||||
- [ ] 6.4 通道阈值新周期复机条件去掉开关分支,固定为新规则复机条件。
|
- [x] 6.4 通道阈值新周期复机条件去掉开关分支,固定为新规则复机条件。
|
||||||
|
|
||||||
## 7. 实名轮询
|
## 7. 实名轮询
|
||||||
|
|
||||||
- [ ] 7.1 `polling_realname_handler.go`:删除按行业卡跳过;卡所属账户未开实名管控时跳过查询并按正常间隔重入队;账户查询失败按可恢复失败处理。
|
- [x] 7.1 `polling_realname_handler.go`:删除按行业卡跳过;卡所属账户未开实名管控时跳过查询并按正常间隔重入队;账户查询失败按可恢复失败处理。(与提案不同:未开管控账户中,卡未实名且所在资产有等待实名激活的待生效套餐时照常查询,避免后台囤货套餐无法激活;规范已同步补充)
|
||||||
|
|
||||||
## 8. 第二次发布的编译与验证
|
## 8. 第二次发布的编译与验证
|
||||||
|
|
||||||
- [ ] 8.1 `gofmt -w`;`go build ./cmd/api ./cmd/worker` 通过;全仓检索无 `UnifiedRuleEnabled` 残留;`openspec validate` 与 `openspec doctor --json` 通过。
|
- [x] 8.1 `gofmt -w`;`go build ./cmd/api ./cmd/worker` 通过;全仓检索无 `UnifiedRuleEnabled` 残留;`openspec validate` 与 `openspec doctor --json` 通过。(validate 通过;当前 openspec 无 doctor 命令)
|
||||||
- [ ] 8.2 测试环境:未开管控账户不查实名;开启管控账户的行业卡查实名并按未实名停机;通道阈值新周期解锁后按新规则复机或只解锁;停复机行为与第一次发布开启总开关后一致。
|
- [ ] 8.2 测试环境:未开管控账户不查实名;开启管控账户的行业卡查实名并按未实名停机;通道阈值新周期解锁后按新规则复机或只解锁;停复机行为与第一次发布开启总开关后一致。
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ type Config struct {
|
|||||||
WeCom WeComConfig `mapstructure:"wecom"`
|
WeCom WeComConfig `mapstructure:"wecom"`
|
||||||
Approval ApprovalConfig `mapstructure:"approval"`
|
Approval ApprovalConfig `mapstructure:"approval"`
|
||||||
TrafficCalc TrafficCalcConfig `mapstructure:"traffic_calc"`
|
TrafficCalc TrafficCalcConfig `mapstructure:"traffic_calc"`
|
||||||
StopResume StopResumeConfig `mapstructure:"stop_resume"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 卡流量计量模式取值。
|
// 卡流量计量模式取值。
|
||||||
@@ -57,14 +56,6 @@ type TrafficCalcConfig struct {
|
|||||||
FlowSnapshotEnabled bool `mapstructure:"flow_snapshot_enabled"`
|
FlowSnapshotEnabled bool `mapstructure:"flow_snapshot_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopResumeConfig 停复机规则配置。
|
|
||||||
type StopResumeConfig struct {
|
|
||||||
// UnifiedRuleEnabled 停复机新规则总开关:开启后复机不看停机原因、实名按通道实名管控判断、
|
|
||||||
// 暂停轮询的卡不自动停复机;关闭时与原有行为完全一致。
|
|
||||||
// 环境变量:JUNHONG_STOP_RESUME_UNIFIED_RULE_ENABLED
|
|
||||||
UnifiedRuleEnabled bool `mapstructure:"unified_rule_enabled"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServerConfig HTTP 服务器配置
|
// ServerConfig HTTP 服务器配置
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
Address string `mapstructure:"address"` // 例如 ":3000"
|
Address string `mapstructure:"address"` // 例如 ":3000"
|
||||||
|
|||||||
@@ -161,10 +161,6 @@ traffic_calc:
|
|||||||
cards: "" # 灰度名单,如 device:15276,iot_card:103154(环境变量:JUNHONG_TRAFFIC_CALC_CARDS)
|
cards: "" # 灰度名单,如 device:15276,iot_card:103154(环境变量:JUNHONG_TRAFFIC_CALC_CARDS)
|
||||||
flow_snapshot_enabled: true # 未切换卡写日快照(环境变量:JUNHONG_TRAFFIC_FLOW_SNAPSHOT_ENABLED)
|
flow_snapshot_enabled: true # 未切换卡写日快照(环境变量:JUNHONG_TRAFFIC_FLOW_SNAPSHOT_ENABLED)
|
||||||
|
|
||||||
# 停复机规则配置
|
|
||||||
stop_resume:
|
|
||||||
unified_rule_enabled: false # 停复机新规则总开关(环境变量:JUNHONG_STOP_RESUME_UNIFIED_RULE_ENABLED)
|
|
||||||
|
|
||||||
# 企业微信 Adapter 配置
|
# 企业微信 Adapter 配置
|
||||||
wecom:
|
wecom:
|
||||||
base_url: "https://qyapi.weixin.qq.com"
|
base_url: "https://qyapi.weixin.qq.com"
|
||||||
|
|||||||
@@ -159,7 +159,6 @@ func bindEnvVariables(v *viper.Viper) {
|
|||||||
"wechat.payment.timeout",
|
"wechat.payment.timeout",
|
||||||
"traffic_calc.mode",
|
"traffic_calc.mode",
|
||||||
"traffic_calc.cards",
|
"traffic_calc.cards",
|
||||||
"stop_resume.unified_rule_enabled",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, key := range bindings {
|
for _, key := range bindings {
|
||||||
|
|||||||
@@ -74,11 +74,10 @@ const (
|
|||||||
StopReasonArrears = "arrears" // 欠费
|
StopReasonArrears = "arrears" // 欠费
|
||||||
StopReasonProtectPeriod = "protect_period" // 保护期一致性检查自动停机(保护期已下线,不再产生新值;保留供历史数据与开放接口映射)
|
StopReasonProtectPeriod = "protect_period" // 保护期一致性检查自动停机(保护期已下线,不再产生新值;保留供历史数据与开放接口映射)
|
||||||
StopReasonNoPackage = "no_package" // 停机原因:无有效套餐
|
StopReasonNoPackage = "no_package" // 停机原因:无有效套餐
|
||||||
StopReasonNotRealname = "not_realname" // 停机原因:非行业卡且未完成实名认证
|
StopReasonNotRealname = "not_realname" // 停机原因:卡所属通道开启实名管控且卡未完成实名认证
|
||||||
StopReasonCarrierStopped = "carrier_stopped" // 停机原因:运营商/Gateway 侧主动停机
|
StopReasonCarrierStopped = "carrier_stopped" // 停机原因:运营商/Gateway 侧主动停机
|
||||||
// StopReasonChannelThreshold 是运营商通道流量阈值达量停机的停因。
|
// StopReasonChannelThreshold 是运营商通道流量阈值达量停机的停因。
|
||||||
// 它既不属于轮询停因(isPollingStopReason),也不参与设备维度扩散(isDeviceScopeReason):
|
// 它不参与设备维度扩散(isDeviceScopeReason):周期内持锁拒绝一切复机,新周期解锁后按停复机规则复机。
|
||||||
// 周期内必须拒绝一切复机,复机只由通道阈值的周期处理发起。
|
|
||||||
StopReasonChannelThreshold = "channel_threshold"
|
StopReasonChannelThreshold = "channel_threshold"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user