chore: 初始开源发布

iotlcm-worker-svr:异步执行中枢(RabbitMQ 消费者 + 全量同步编排)。

- Go module:iotlcm-worker-svr → git.5iot.com/iotlcm-public/iotlcm-worker-svr(214 文件)
- 凭据与内部标识脱敏:MySQL/Redis 凭据、私网与生产 IP、内网域名、
  库名 xiujie_admin → iotlcm、哨兵名占位
- 159 处真实手机号 → 合法 11 位编造号(保留号段与后 4 位,维持互异,
  不破坏「校验 MSISDN 格式」等测试断言)
- 移除内部资产:CI 触发器、Jenkins 触发测试文档、预演门禁与镜像
  digest 记录
- Dockerfile 改用公共基础镜像(golang:1.25-alpine → alpine:3.23),
  移除写死的 git 凭据
- README 重写(架构/全量同步模型/配置/已知失败),补 Apache-2.0
  LICENSE,LF 行尾 + .gitattributes

已知:3 个测试包在源仓同一提交上同样失败(client/upstream、
service/allocate、service/inbound),属既有缺陷,本仓未引入亦未修复。
This commit is contained in:
MiFaZhan
2026-10-22 16:37:56 +08:00
commit 2bec257289
361 changed files with 71524 additions and 0 deletions
@@ -0,0 +1,413 @@
package renewchange
import (
"encoding/json"
"reflect"
"testing"
"time"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/util/gconv"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/do"
rc "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/repository/renewchange"
)
func TestParseExecutionContractReadsConfirmedFlatContract(t *testing.T) {
main := &rc.BatchMainRow{
BatchNo: "REN20260803100250ABCDEFGH",
BatchType: consts.BatchTypeRenew,
DeptId: 100,
CreatedDept: 100,
CreatedBy: 1,
TenantId: "000000",
BatchInfo: gjson.New(map[string]any{
"customerAgentId": 200,
"packageCode": "PKG001",
"packageName": "Year package",
"renewUnitPrice": 10.25,
"quantity": 2,
"durationMonths": 12,
"giftDurationMonths": 1,
"offlineContractCompleted": true,
"buyerNote": "buyer",
"adminNote": "admin",
}),
}
contract, err := ParseExecutionContract(main)
if err != nil {
t.Fatalf("ParseExecutionContract returned error: %v", err)
}
if contract.DurationDisplayValue != 1 || contract.DurationMonths != 12 || contract.GiftDurationMonths != 1 {
t.Fatalf("duration = %d/%d+%d, want display 1 and actual 12+1", contract.DurationDisplayValue, contract.DurationMonths, contract.GiftDurationMonths)
}
if contract.CustomerAgentID != 200 || contract.SellerAgentID != 100 {
t.Fatalf("buyer/seller = %d/%d, want 200/100", contract.CustomerAgentID, contract.SellerAgentID)
}
if contract.TotalAmount() != 20.5 {
t.Fatalf("TotalAmount = %.2f, want 20.50", contract.TotalAmount())
}
if contract.ItemPrice() != 10.25 {
t.Fatalf("ItemPrice = %.2f, want 10.25", contract.ItemPrice())
}
}
func TestParseExecutionContractRenewDurationValueOverridesLegacyMonthSnapshot(t *testing.T) {
main := &rc.BatchMainRow{
BatchNo: "REN20260803100250LEGACY",
BatchType: consts.BatchTypeRenew,
DeptId: 100,
CreatedDept: 100,
CreatedBy: 1,
TenantId: "000000",
BatchInfo: gjson.New(map[string]any{
"customerAgentId": 200,
"packageCode": "PKG001",
"packageName": "Year package",
"renewUnitPrice": 10.25,
"quantity": 1,
"durationValue": 1,
"durationYears": 1,
"durationMonths": 1,
"actualDurationMonths": 1,
"billingMethod": consts.BillingMethodMonth,
"renewDurationUnit": consts.RenewDurationUnitMonth,
"offlineContractCompleted": true,
}),
}
contract, err := ParseExecutionContract(main)
if err != nil {
t.Fatalf("ParseExecutionContract returned error: %v", err)
}
if contract.RenewDurationUnit != consts.RenewDurationUnitYear {
t.Fatalf("renew duration unit = %q, want year", contract.RenewDurationUnit)
}
if contract.DurationDisplayValue != 1 || contract.DurationMonths != 12 {
t.Fatalf("duration = %d/%d, want display 1 and actual 12 months", contract.DurationDisplayValue, contract.DurationMonths)
}
}
func TestParseExecutionContractFallsBackToLegacyNestedSnapshot(t *testing.T) {
main := &rc.BatchMainRow{
BatchNo: "CHG20260803100250ABCDEFGH",
BatchType: consts.BatchTypeChange,
DeptId: 100,
CreatedDept: 100,
CreatedBy: 1,
TenantId: "000000",
BatchInfo: gjson.New(map[string]any{
"changePriceSnapshot": map[string]any{
"customerAgentId": 200,
"newPackageCode": "PKG002",
"newPackageName": "Changed package",
"changeUnitPrice": 8.5,
},
"quantity": 1,
"durationMonths": 24,
"giftDurationMonths": 0,
"offlineContractCompleted": true,
}),
}
contract, err := ParseExecutionContract(main)
if err != nil {
t.Fatalf("ParseExecutionContract returned error: %v", err)
}
if contract.PackageCode != "PKG002" || contract.UnitPrice != 8.5 {
t.Fatalf("legacy snapshot contract = %s/%.2f, want PKG002/8.50", contract.PackageCode, contract.UnitPrice)
}
}
func TestParseExecutionContractNormalizesSourceBatchBySourceType(t *testing.T) {
tests := []struct {
name string
sourceType int
mainSourceBatchNo string
infoSourceBatchNo string
want string
}{
{
name: "input self reference is not a business source batch",
sourceType: consts.BatchSourceTypeBatchInput,
mainSourceBatchNo: "REN20260803161502D52JJU6B",
want: "",
},
{
name: "file source ignores legacy source value",
sourceType: consts.BatchSourceTypeFile,
mainSourceBatchNo: "REN20260803161502D52JJU6B",
infoSourceBatchNo: "OUT20260803STALE",
want: "",
},
{
name: "batch selection keeps confirmed source batch",
sourceType: consts.BatchSourceTypeBatchSelect,
mainSourceBatchNo: "OUT20260803MAIN",
infoSourceBatchNo: "OUT20260803CONFIRMED",
want: "OUT20260803CONFIRMED",
},
{
name: "batch selection falls back to main source batch",
sourceType: consts.BatchSourceTypeBatchSelect,
mainSourceBatchNo: "OUT20260803MAIN",
want: "OUT20260803MAIN",
},
{
name: "batch selection repairs self reference from main source batch",
sourceType: consts.BatchSourceTypeBatchSelect,
mainSourceBatchNo: "OUT20260803MAIN",
infoSourceBatchNo: "REN20260803161502D52JJU6B",
want: "OUT20260803MAIN",
},
{
name: "legacy self reference is normalized to empty",
sourceType: consts.BatchSourceTypeBatchSelect,
mainSourceBatchNo: "REN20260803161502D52JJU6B",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
info := map[string]any{
"customerAgentId": 200,
"packageCode": "PKG001",
"packageName": "Year package",
"renewUnitPrice": 10.25,
"quantity": 1,
"durationMonths": 12,
"offlineContractCompleted": true,
}
if tt.infoSourceBatchNo != "" {
info["sourceBatchNo"] = tt.infoSourceBatchNo
}
main := &rc.BatchMainRow{
BatchNo: "REN20260803161502D52JJU6B",
BatchType: consts.BatchTypeRenew,
SourceType: tt.sourceType,
SourceBatchNo: tt.mainSourceBatchNo,
DeptId: 100,
CreatedDept: 100,
CreatedBy: 1,
TenantId: "000000",
BatchInfo: gjson.New(info),
}
contract, err := ParseExecutionContract(main)
if err != nil {
t.Fatalf("ParseExecutionContract returned error: %v", err)
}
if contract.SourceBatchNo != tt.want {
t.Fatalf("SourceBatchNo = %q, want %q", contract.SourceBatchNo, tt.want)
}
})
}
}
func TestParseExecutionContractRejectsMissingConfirmedFields(t *testing.T) {
base := validExecutionContract()
cases := []struct {
name string
mutate func(*ExecutionContract)
}{
{name: "customer", mutate: func(c *ExecutionContract) { c.CustomerAgentID = 0 }},
{name: "duration", mutate: func(c *ExecutionContract) { c.DurationMonths = 0 }},
{name: "quantity", mutate: func(c *ExecutionContract) { c.Quantity = 0 }},
{name: "package", mutate: func(c *ExecutionContract) { c.PackageCode = "" }},
{name: "offline contract", mutate: func(c *ExecutionContract) { c.OfflineContractCompleted = false }},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := *base
tc.mutate(&got)
if err := got.Validate(); err == nil {
t.Fatalf("Validate accepted missing %s", tc.name)
}
})
}
}
func TestBuildOrderDocumentsUseAggregatePurchasePeriodPrice(t *testing.T) {
contract := validExecutionContract()
contract.UnitPrice = 10.25
contract.Quantity = 2
contract.DurationMonths = 12
contract.GiftDurationMonths = 1
now := time.Date(2026, 8, 3, 10, 2, 50, 0, time.Local)
orderSN := "SO20260803100250ABCDEFGH"
order, err := BuildOrderDO(contract, orderSN, now)
if err != nil {
t.Fatalf("BuildOrderDO returned error: %v", err)
}
if gconv.String(order.OrderSn) != orderSN {
t.Fatalf("order_sn = %q, want %q", order.OrderSn, orderSN)
}
if gconv.Int64(order.CustomerAgentId) != 200 || gconv.Int64(order.AgentId) != 100 {
t.Fatalf("order buyer/seller = %v/%v, want 200/100", order.CustomerAgentId, order.AgentId)
}
if gconv.Int(order.OrderType) != consts.OrderTypeCardRenew {
t.Fatalf("order_type = %v, want renew", order.OrderType)
}
if gconv.Float64(order.TotalAmount) != 246 || gconv.Float64(order.ProductAmount) != 246 || gconv.Float64(order.PaidAmount) != 246 {
t.Fatalf("order amount fields = %v/%v/%v, want 246", order.TotalAmount, order.ProductAmount, order.PaidAmount)
}
if order.CreatedAt == nil || !order.CreatedAt.Time.Equal(now) || order.UpdatedAt == nil || !order.UpdatedAt.Time.Equal(now) {
t.Fatalf("order audit times = %v/%v, want %v", order.CreatedAt, order.UpdatedAt, now)
}
beginAt := time.Date(2026, 8, 15, 0, 0, 0, 0, time.Local)
endAt := contract.ServiceEnd(beginAt)
item, err := BuildOrderItemDO(contract, OrderProductContract{
ID: 300, Code: "PKG001", Name: "Year package", Type: 1,
}, 900, orderSN, beginAt, endAt, now)
if err != nil {
t.Fatalf("BuildOrderItemDO returned error: %v", err)
}
if gconv.Float64(item.Price) != 123 {
t.Fatalf("item price = %v, want totalAmount/quantity = 123", item.Price)
}
if gconv.Int(item.Quantity) != 2 {
t.Fatalf("item quantity = %v, want 2", item.Quantity)
}
if item.CreatedAt == nil || !item.CreatedAt.Time.Equal(now) || item.UpdatedAt == nil || !item.UpdatedAt.Time.Equal(now) {
t.Fatalf("item audit times = %v/%v, want %v", item.CreatedAt, item.UpdatedAt, now)
}
if got := item.ServiceEndTime.Time; !got.Equal(time.Date(2027, 9, 15, 0, 0, 0, 0, time.Local)) {
t.Fatalf("service end = %s, want 2027-09-15", got)
}
if item.CreatedAt == nil || item.UpdatedAt == nil || !item.CreatedAt.Time.Equal(now) || !item.UpdatedAt.Time.Equal(now) {
t.Fatalf("order item audit fields = %v/%v, want %v", item.CreatedAt, item.UpdatedAt, now)
}
var extra map[string]any
if err = json.Unmarshal([]byte(gconv.String(item.ExtraSkuData)), &extra); err != nil {
t.Fatalf("extra_sku_data is invalid JSON: %v", err)
}
if gconv.String(extra["outboundBatchNo"]) != contract.BatchNo || gconv.Int(extra["durationMonths"]) != 12 || gconv.Int(extra["giftDurationMonths"]) != 1 {
t.Fatalf("extra duration = %v+%v, want 12+1", extra["durationMonths"], extra["giftDurationMonths"])
}
if gconv.Float64(extra["amount"]) != 246 {
t.Fatalf("extra amount = %v, want 246", extra["amount"])
}
}
func TestBuildOrderUsesChangeOrderType(t *testing.T) {
contract := validExecutionContract()
contract.BatchType = consts.BatchTypeChange
order, err := BuildOrderDO(contract, "SO20260803100250CHANGE01", time.Date(2026, 8, 3, 10, 2, 50, 0, time.Local))
if err != nil {
t.Fatalf("BuildOrderDO returned error: %v", err)
}
if gconv.Int(order.OrderType) != consts.OrderTypeCardChange {
t.Fatalf("order_type = %v, want change", order.OrderType)
}
}
func TestBuildOrderUsesBalancePaymentSnapshot(t *testing.T) {
contract := validExecutionContract()
contract.OfflineContractCompleted = false
contract.PaymentMode = "balance"
contract.PayType = consts.PayTypeBalance
contract.PayAmount = 120
contract.BalanceAmount = 120
contract.BalanceDetailSerialNo = "RENPAYREN20260803100250ABCDEFGH"
contract.UnitPrice = 10
contract.Quantity = 1
contract.DurationMonths = 12
order, err := BuildOrderDO(contract, "SO20260803100250ABCDEFGH", time.Date(2026, 8, 3, 10, 2, 50, 0, time.Local))
if err != nil {
t.Fatalf("BuildOrderDO returned error: %v", err)
}
if gconv.Int(order.PayType) != consts.PayTypeBalance {
t.Fatalf("pay_type = %v, want balance", order.PayType)
}
if gconv.String(order.PayOrderSn) != contract.BalanceDetailSerialNo {
t.Fatalf("pay_order_sn = %q, want %q", order.PayOrderSn, contract.BalanceDetailSerialNo)
}
if gconv.Float64(order.Balance) != 120 || gconv.Float64(order.OfflinePaidAmount) != 0 {
t.Fatalf("balance/offline = %v/%v, want 120/0", order.Balance, order.OfflinePaidAmount)
}
}
func TestBalancePaymentSnapshotMustMatchTotalAmount(t *testing.T) {
contract := validExecutionContract()
contract.OfflineContractCompleted = false
contract.PaymentMode = "balance"
contract.PayType = consts.PayTypeBalance
contract.PayAmount = 1
contract.BalanceAmount = 1
contract.BalanceDetailSerialNo = "RENPAYREN20260803100250ABCDEFGH"
if err := contract.Validate(); err == nil {
t.Fatal("Validate accepted balance amount that does not match total amount")
}
}
func TestBuildOrderRejectsEmptyOrderNumber(t *testing.T) {
if _, err := BuildOrderDO(validExecutionContract(), "", time.Now()); err == nil {
t.Fatal("BuildOrderDO accepted an empty order number")
}
}
func TestBuildRenewCardUpdateOnlySetsRenewFields(t *testing.T) {
contract := validExecutionContract()
endAt := time.Date(2027, 8, 3, 0, 0, 0, 0, time.Local)
now := time.Date(2026, 8, 3, 10, 2, 50, 0, time.Local)
update, err := BuildRenewCardUpdate(contract, endAt, consts.PackageStatusEffective, now)
if err != nil {
t.Fatalf("BuildRenewCardUpdate returned error: %v", err)
}
if update.FlowPkgEndAt == nil || !update.FlowPkgEndAt.Time.Equal(endAt) {
t.Fatalf("flow_pkg_end_at = %v, want %v", update.FlowPkgEndAt, endAt)
}
if gconv.Int(update.PackageStatus) != consts.PackageStatusEffective || gconv.Int64(update.UpdatedBy) != contract.OperatorUserID {
t.Fatalf("package_status/updated_by = %v/%v", update.PackageStatus, update.UpdatedBy)
}
if update.UpdatedAt == nil || !update.UpdatedAt.Time.Equal(now) {
t.Fatalf("updated_at = %v, want %v", update.UpdatedAt, now)
}
assertOnlyRenewCardFieldsSet(t, update)
}
func validExecutionContract() *ExecutionContract {
return &ExecutionContract{
BatchNo: "REN20260803100250ABCDEFGH",
BatchType: consts.BatchTypeRenew,
TenantID: "000000",
CustomerAgentID: 200,
SellerAgentID: 100,
CreatedDeptID: 100,
OperatorUserID: 1,
PackageCode: "PKG001",
PackageName: "Year package",
UnitPrice: 10,
Quantity: 1,
DurationMonths: 12,
GiftDurationMonths: 0,
OfflineContractCompleted: true,
}
}
func assertOnlyRenewCardFieldsSet(t *testing.T, update *do.TSimCardInfo) {
t.Helper()
allowed := map[string]bool{"FlowPkgEndAt": true, "PackageStatus": true, "UpdatedBy": true, "UpdatedAt": true}
value := reflect.ValueOf(update).Elem()
typ := value.Type()
for i := 0; i < value.NumField(); i++ {
fieldType := typ.Field(i)
if fieldType.Name == "Meta" || allowed[fieldType.Name] {
continue
}
field := value.Field(i)
switch field.Kind() {
case reflect.Interface, reflect.Pointer, reflect.Map, reflect.Slice:
if !field.IsNil() {
t.Fatalf("unexpected renew card field set: %s", fieldType.Name)
}
}
}
}