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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user