feat: 实现单卡资产分配与回收功能
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 4m45s

- 新增单卡分配/回收 API(支持 ICCID 列表、号段范围、筛选条件三种选卡方式)
- 新增资产分配记录查询 API(支持多条件筛选和分页)
- 新增 AssetAllocationRecord 模型、Store、Service、Handler 完整实现
- 扩展 IotCardStore 新增批量更新、号段查询、筛选查询等方法
- 修复 GORM Callback 处理 slice 类型(BatchCreate)的问题
- 新增完整的单元测试和集成测试
- 同步 OpenSpec 规范并归档 change
This commit is contained in:
2026-01-24 15:46:15 +08:00
parent a924e63e68
commit 194078674a
33 changed files with 2785 additions and 92 deletions

View File

@@ -59,6 +59,15 @@ const (
CodeWithdrawalNotFound = 1052 // 提现申请不存在
CodeWalletNotFound = 1053 // 钱包不存在
// IoT 卡相关错误 (1070-1089)
CodeIotCardNotFound = 1070 // IoT 卡不存在
CodeIotCardBoundToDevice = 1071 // IoT 卡已绑定设备
CodeIotCardStatusNotAllowed = 1072 // 卡状态不允许此操作
CodeAssetAllocationRecordNotFound = 1073 // 分配记录不存在
CodeNotDirectSubordinate = 1074 // 非直属下级店铺
CodeCannotAllocateToSelf = 1075 // 不能分配给自己
CodeCannotRecallFromSelf = 1076 // 不能从自己回收
// 服务端错误 (2000-2999) -> 5xx HTTP 状态码
CodeInternalError = 2001 // 内部服务器错误
CodeDatabaseError = 2002 // 数据库错误
@@ -114,6 +123,13 @@ var allErrorCodes = []int{
CodeInsufficientBalance,
CodeWithdrawalNotFound,
CodeWalletNotFound,
CodeIotCardNotFound,
CodeIotCardBoundToDevice,
CodeIotCardStatusNotAllowed,
CodeAssetAllocationRecordNotFound,
CodeNotDirectSubordinate,
CodeCannotAllocateToSelf,
CodeCannotRecallFromSelf,
CodeInternalError,
CodeDatabaseError,
CodeRedisError,
@@ -132,55 +148,62 @@ func init() {
// errorMessages 错误消息映射表(中文)
var errorMessages = map[int]string{
CodeSuccess: "成功",
CodeInvalidParam: "参数验证失败",
CodeMissingToken: "缺失认证令牌",
CodeInvalidToken: "无效或过期的令牌",
CodeUnauthorized: "未授权访问",
CodeForbidden: "禁止访问",
CodeNotFound: "资源未找到",
CodeConflict: "资源冲突",
CodeTooManyRequests: "请求过多,请稍后重试",
CodeRequestTooLarge: "请求体过大",
CodeAccountNotFound: "账号不存在",
CodeAccountDisabled: "账号已禁用",
CodeAccountDeleted: "账号已删除",
CodeUsernameExists: "用户名已存在",
CodePhoneExists: "手机号已存在",
CodeInvalidPassword: "密码格式不正确",
CodePasswordTooWeak: "密码强度不足",
CodeParentIDRequired: "非 root 用户必须提供上级账号",
CodeInvalidParentID: "上级账号不存在或无效",
CodeCannotModifyParent: "禁止修改上级账号",
CodeCannotModifyUserType: "禁止修改用户类型",
CodeRoleNotFound: "角色不存在",
CodeRoleNameExists: "角色名称已存在",
CodePermissionNotFound: "权限不存在",
CodePermCodeExists: "权限编码已存在",
CodeInvalidPermCode: "权限编码格式不正确(应为 module:action 格式)",
CodeRoleAlreadyAssigned: "角色已分配",
CodePermAlreadyAssigned: "权限已分配",
CodeShopNotFound: "店铺不存在",
CodeShopCodeExists: "店铺编号已存在",
CodeShopLevelExceeded: "店铺层级不能超过 7 级",
CodeEnterpriseNotFound: "企业不存在",
CodeEnterpriseCodeExists: "企业编号已存在",
CodeCustomerNotFound: "个人客户不存在",
CodeCustomerPhoneExists: "个人客户手机号已存在",
CodeInvalidStatus: "状态不允许此操作",
CodeInsufficientBalance: "余额不足",
CodeWithdrawalNotFound: "提现申请不存在",
CodeWalletNotFound: "钱包不存在",
CodeInvalidCredentials: "用户名或密码错误",
CodeAccountLocked: "账号已锁定",
CodePasswordExpired: "密码已过期",
CodeInvalidOldPassword: "旧密码错误",
CodeInternalError: "内部服务器错误",
CodeDatabaseError: "数据库错误",
CodeRedisError: "缓存服务错误",
CodeServiceUnavailable: "服务暂时不可用",
CodeTimeout: "请求超时",
CodeTaskQueueError: "任务队列错误",
CodeSuccess: "成功",
CodeInvalidParam: "参数验证失败",
CodeMissingToken: "缺失认证令牌",
CodeInvalidToken: "无效或过期的令牌",
CodeUnauthorized: "未授权访问",
CodeForbidden: "禁止访问",
CodeNotFound: "资源未找到",
CodeConflict: "资源冲突",
CodeTooManyRequests: "请求过多,请稍后重试",
CodeRequestTooLarge: "请求体过大",
CodeAccountNotFound: "账号不存在",
CodeAccountDisabled: "账号已禁用",
CodeAccountDeleted: "账号已删除",
CodeUsernameExists: "用户名已存在",
CodePhoneExists: "手机号已存在",
CodeInvalidPassword: "密码格式不正确",
CodePasswordTooWeak: "密码强度不足",
CodeParentIDRequired: "非 root 用户必须提供上级账号",
CodeInvalidParentID: "上级账号不存在或无效",
CodeCannotModifyParent: "禁止修改上级账号",
CodeCannotModifyUserType: "禁止修改用户类型",
CodeRoleNotFound: "角色不存在",
CodeRoleNameExists: "角色名称已存在",
CodePermissionNotFound: "权限不存在",
CodePermCodeExists: "权限编码已存在",
CodeInvalidPermCode: "权限编码格式不正确(应为 module:action 格式)",
CodeRoleAlreadyAssigned: "角色已分配",
CodePermAlreadyAssigned: "权限已分配",
CodeShopNotFound: "店铺不存在",
CodeShopCodeExists: "店铺编号已存在",
CodeShopLevelExceeded: "店铺层级不能超过 7 级",
CodeEnterpriseNotFound: "企业不存在",
CodeEnterpriseCodeExists: "企业编号已存在",
CodeCustomerNotFound: "个人客户不存在",
CodeCustomerPhoneExists: "个人客户手机号已存在",
CodeInvalidStatus: "状态不允许此操作",
CodeInsufficientBalance: "余额不足",
CodeWithdrawalNotFound: "提现申请不存在",
CodeWalletNotFound: "钱包不存在",
CodeIotCardNotFound: "IoT 卡不存在",
CodeIotCardBoundToDevice: "IoT 卡已绑定设备,不能单独操作",
CodeIotCardStatusNotAllowed: "卡状态不允许此操作",
CodeAssetAllocationRecordNotFound: "分配记录不存在",
CodeNotDirectSubordinate: "只能操作直属下级店铺",
CodeCannotAllocateToSelf: "不能分配给自己",
CodeCannotRecallFromSelf: "不能从自己回收",
CodeInvalidCredentials: "用户名或密码错误",
CodeAccountLocked: "账号已锁定",
CodePasswordExpired: "密码已过期",
CodeInvalidOldPassword: "旧密码错误",
CodeInternalError: "内部服务器错误",
CodeDatabaseError: "数据库错误",
CodeRedisError: "缓存服务错误",
CodeServiceUnavailable: "服务暂时不可用",
CodeTimeout: "请求超时",
CodeTaskQueueError: "任务队列错误",
}
// GetMessage 获取错误码对应的消息

View File

@@ -13,6 +13,16 @@ var (
ErrTooManyRequests = errors.New("too many requests")
)
// 预定义业务错误(常用错误可直接引用)
var (
ErrAssetAllocationRecordNotFound = &AppError{Code: CodeAssetAllocationRecordNotFound, Message: "分配记录不存在"}
ErrNotDirectSubordinate = &AppError{Code: CodeNotDirectSubordinate, Message: "只能操作直属下级店铺"}
ErrIotCardBoundToDevice = &AppError{Code: CodeIotCardBoundToDevice, Message: "IoT 卡已绑定设备,不能单独操作"}
ErrIotCardStatusNotAllowed = &AppError{Code: CodeIotCardStatusNotAllowed, Message: "卡状态不允许此操作"}
ErrCannotAllocateToSelf = &AppError{Code: CodeCannotAllocateToSelf, Message: "不能分配给自己"}
ErrCannotRecallFromSelf = &AppError{Code: CodeCannotRecallFromSelf, Message: "不能从自己回收"}
)
// AppError 表示带错误码的应用错误
type AppError struct {
Code int // 应用错误码

View File

@@ -2,6 +2,7 @@ package gorm
import (
"context"
"reflect"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/logger"
@@ -236,13 +237,47 @@ func RegisterDataPermissionCallback(db *gorm.DB, shopStore ShopStoreInterface) e
func RegisterSetCreatorUpdaterCallback(db *gorm.DB) error {
err := db.Callback().Create().Before("gorm:create").Register("set_creator_updater", func(tx *gorm.DB) {
ctx := tx.Statement.Context
if userID, ok := tx.Statement.Context.Value(constants.ContextKeyUserID).(uint); ok {
if f := tx.Statement.Schema; f != nil {
if c, ok := f.FieldsByName["Creator"]; ok {
_ = c.Set(ctx, tx.Statement.ReflectValue, userID)
userID, ok := tx.Statement.Context.Value(constants.ContextKeyUserID).(uint)
if !ok || tx.Statement.Schema == nil {
return
}
creatorField, hasCreator := tx.Statement.Schema.FieldsByName["Creator"]
updaterField, hasUpdater := tx.Statement.Schema.FieldsByName["Updater"]
if !hasCreator && !hasUpdater {
return
}
rv := tx.Statement.ReflectValue
switch rv.Kind() {
case reflect.Slice, reflect.Array:
for i := 0; i < rv.Len(); i++ {
elem := rv.Index(i)
if elem.Kind() == reflect.Ptr {
elem = elem.Elem()
}
if u, ok := f.FieldsByName["Updater"]; ok {
_ = u.Set(ctx, tx.Statement.ReflectValue, userID)
if hasCreator {
_ = creatorField.Set(ctx, elem, userID)
}
if hasUpdater {
_ = updaterField.Set(ctx, elem, userID)
}
}
case reflect.Struct:
if hasCreator {
_ = creatorField.Set(ctx, rv, userID)
}
if hasUpdater {
_ = updaterField.Set(ctx, rv, userID)
}
case reflect.Ptr:
elem := rv.Elem()
if elem.Kind() == reflect.Struct {
if hasCreator {
_ = creatorField.Set(ctx, elem, userID)
}
if hasUpdater {
_ = updaterField.Set(ctx, elem, userID)
}
}
}