左晓为主开发手持机充值管理机
编辑 | blame | 历史 | 原始文档

充值系统 (Recharge System)

这是一个基于Android的充值系统项目,采用模块化架构设计,使用Room数据库进行数据持久化存储。

项目结构

项目包含以下模块:

  • app: 主应用模块
  • baselibrary: 基础库,包含基本工具类和通用组件
  • generallibrary: 通用功能库,包含数据库操作等通用功能
  • henanlibrary: 河南地区特定功能模块
  • qihealonelibrary: 齐河单机版功能模块
  • qiheonlinelibrary: 齐河在线版功能模块
  • ocridcardlibrary: 身份证识别模块
  • easysocket: Socket通信模块
  • pickerviewlibrary: 选择器视图库

技术栈

  • 开发语言:Kotlin & Java
  • 数据库:Room
  • 网络通信:Retrofit & Socket
  • 依赖注入:未使用
  • 异步处理:RxJava
  • 权限管理:XXPermissions
  • 视图绑定:
  • ViewBinding:用于安全高效地访问视图
  • DataBinding:用于数据驱动UI的MVVM架构实现

视图绑定配置

ViewBinding

在模块级build.gradle中启用ViewBinding:
gradle android { buildFeatures { viewBinding true } }

使用示例:
```kotlin
class ExampleActivity : AppCompatActivity() {
private lateinit var binding: ActivityExampleBinding

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityExampleBinding.inflate(layoutInflater)
    setContentView(binding.root)

    // 直接访问视图
    binding.textView.text = "Hello ViewBinding"
}

}
```

DataBinding

在模块级build.gradle中启用DataBinding:
gradle android { buildFeatures { dataBinding true } }

使用示例:
1. 布局文件:
xml <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="user" type="com.example.User" /> </data> <LinearLayout> <TextView android:text="@{user.name}" /> </LinearLayout> </layout>

  1. Activity中使用:
    ```kotlin
    class ExampleActivity : AppCompatActivity() {
    private lateinit var binding: ActivityExampleBinding

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = DataBindingUtil.setContentView(this, R.layout.activity_example)

    binding.user = User("张三")
    binding.lifecycleOwner = this
    
    }
    }
    ```

最佳实践

  • ViewBinding:
  • 用于简单的视图访问场景
  • 替代findViewById,避免空指针异常
  • 编译时类型安全
  • DataBinding:

  • 用于实现MVVM架构
  • 数据驱动UI更新
  • 双向绑定支持
  • 自定义绑定适配器
  • 表达式支持

RecyclerView 列表为空时的实现

在 RecyclerView 适配器中,当列表数据为空时,显示一个空视图(EmptyView)的实现方式:

  1. 继承 BaseRecycleAdapter:
    kotlin class YourAdapter : BaseRecycleAdapter<RecyclerView.ViewHolder>() { // 实现必要的方法 }

  2. 在适配器中定义视图类型常量(已在 BaseRecycleAdapter 中定义):
    kotlin companion object { const val VIEW_TYPE_ITEM = 1 const val VIEW_TYPE_EMPTY = 0 }

  3. 重写 getItemViewType 方法:
    kotlin override fun getItemViewType(position: Int): Int { if (dataList.isEmpty()) { return VIEW_TYPE_EMPTY } return VIEW_TYPE_ITEM }

  4. 重写 getItemCount 方法:
    kotlin override fun getItemCount(): Int { if (dataList.isEmpty()) { return 1 // 返回1表示显示空视图 } return dataList.size }

  5. 在 onCreateViewHolder 中处理不同类型的视图:
    kotlin override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { if (viewType == VIEW_TYPE_EMPTY) { val emptyView: ItemNoMoreBinding = DataBindingUtil.inflate( (parent.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)!!, R.layout.item_no_more, parent, false ) return ViewHolderEmpty(emptyView) } else { val binding = ItemListBinding.inflate( parent.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater, parent, false ) return ItemViewHolder(binding.root) } }

  6. 重写 onBindViewHolder 方法:
    ```kotlin
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    if (holder is ViewHolderEmpty) {
    // 空视图不需要绑定数据
    return
    }

    // 绑定列表项数据
    if (holder is ItemViewHolder) {
    val item = dataList[position]
    holder.bind(item)
    }
    }
    ```

  7. 空视图的布局文件示例(item_no_more.xml):
    ```xml
    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
    <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:orientation="vertical"
    android:padding="16dp">

    <ImageView
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:src="@drawable/ic_empty" />
    
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="暂无数据"
        android:textColor="@color/text_gray"
        android:textSize="14sp" />
    


    ```

这种实现方式的优点:
1. 统一的空视图处理逻辑
2. 支持自定义空视图样式
3. 不影响列表正常数据的显示
4. 便于维护和扩展

数据库结构

GeneralLibrary 数据库

@Database(entities = [PassWordCardBean::class, CardData::class, ProjectDataBean::class], version = 1)

主要实体:
- PassWordCardBean: 密码卡数据
- CardData: 卡片数据
- ProjectDataBean: 项目数据

开发环境要求

  • Android Studio Arctic Fox或更高版本
  • JDK 8或更高版本
  • Android SDK API 23或更高版本
  • Gradle 7.0或更高版本

构建与运行

  1. 克隆项目:
    bash git clone [项目地址]

  2. 在Android Studio中打开项目

  3. 同步Gradle文件

  4. 构建项目:
    bash ./gradlew build

  5. 运行应用:

  • 通过Android Studio运行
  • 或使用命令行:./gradlew installDebug

模块说明

BaseLibrary

基础功能库,提供:
- 基础Activity
- 通用工具类
- 基础UI组件

GeneralLibrary

通用功能模块,包含:
- Room数据库实现
- 数据访问对象(DAO)
- 实体类定义

其他模块

  • HenanlLibrary: 河南地区特定功能
  • QiheAloneLibrary: 齐河单机版特定功能
  • QiheOnlineLibrary: 齐河在线版特定功能
  • OCRIDCardLibrary: 身份证识别功能
  • EasySocket: Socket通信实现
  • PickerViewLibrary: 选择器控件

自定义组件使用说明

TitleBar 标题栏组件

TitleBar是一个自定义的标题栏组件,提供了左中右三个位置的文本和图片设置功能。

XML属性配置

<com.dayu.baselibrary.view.TitleBar
    android:id="@+id/titleBar"
    android:layout_width="match_parent"
    android:layout_height="@dimen/dimen_title_height"
    app:leftText="返回"
    app:leftImage="@drawable/ic_back"
    app:centerText="标题"
    app:centerImage="@drawable/ic_logo"
    app:rightText="更多"
    app:rightImage="@drawable/ic_more"/>

代码中使用

  1. 设置文本和图片
    kotlin titleBar.apply { setLeftText("返回") setLeftImage(R.drawable.ic_back) setCenterText("标题") setRightText("更多") setRightImage(R.drawable.ic_more) }

  2. 点击事件监听
    ```kotlin
    // 方式1:使用类型常量(推荐)
    titleBar.setOnItemclickListner(TitleBar.ClickType_LEFT_IMAGE) {
    finish()
    }
    titleBar.setOnItemclickListner(TitleBar.ClickType_RIGHT_TEXT) {
    showMenu()
    }

// 或者使用完整的OnClickListener
titleBar.setOnItemclickListner(TitleBar.ClickType_LEFT_IMAGE, View.OnClickListener {
finish()
})

// 方式2:使用位置和类型常量(已废弃)
titleBar.setOnItemclickListner(TitleBar.IMAGE, TitleBar.LEFT) { finish() }
```

点击类型常量说明

  • ClickType_LEFT_TEXT: 左侧文本点击
  • ClickType_LEFT_IMAGE: 左侧图片点击
  • ClickType_CENTER_TEXT: 中间文本点击
  • ClickType_CENTER_IMAGE: 中间图片点击
  • ClickType_RIGHT_TEXT: 右侧文本点击
  • ClickType_RIGHT_IMAGE: 右侧图片点击

其他功能

// 设置右侧按钮状态
titleBar.setRightStatus(false) // 禁用右侧按钮

// 设置右侧图片可见性
titleBar.setRightIMGVisibility(View.GONE)

// 获取中间的TextView
val titleTextView = titleBar.getTitleTextView()

// 获取右侧布局
val rightLayout = titleBar.getLlRight()

注意事项

  1. 点击事件只有在对应的视图可见时才会生效
  2. 设置文本或图片时,如果传入null或0,对应的视图将被隐藏
  3. 组件默认使用垂直线性布局,确保在布局文件中设置合适的高度

常见使用示例

// 在Activity的initView方法中设置
private fun initView() {
    // 设置返回按钮点击事件
    binding.titleBar.setOnItemclickListner(TitleBar.ClickType_LEFT_IMAGE) {
        finish()
    }
    
    // 设置右侧文本按钮点击事件
    binding.titleBar.setOnItemclickListner(TitleBar.ClickType_RIGHT_TEXT) {
        // 处理右侧按钮点击逻辑
        handleRightButtonClick()
    }
}

支付方式动态获取功能

项目中实现了支付方式的动态获取和显示功能,支持从服务器获取支付方式列表并动态创建RadioButton。

数据结构

// 支付方式数据类
data class PaymentMethod(
    val id: Long,
    val name: String,
    val remarks: String,
    val deleted: Int
)

// 支付方式接口返回数据类
data class PaymentMethodResponse(
    val itemTotal: Any?,
    val obj: List<PaymentMethod>,
    val pageCurr: Any?,
    val pageSize: Any?,
    val pageTotal: Any?
)

使用方式

1. 在Activity中添加支付方式相关属性

class YourActivity : AppCompatActivity() {
    // 支付方式相关属性
    private var paymentMethod: String = "现金"
    private var paymentId: Long = 0
    private var paymentMethodList: List<PaymentMethod> = listOf()
    
    // ... 其他代码
}

2. 获取支付方式列表

/**
 * 获取支付方式列表
 */
private fun getPaymentMethods() {
    ApiManager.getInstance().requestGetLoading(
        this,
        "sell/paymentmethod/get",
        PaymentMethodResponse::class.java,
        null,
        object : SubscriberListener<BaseResponse<PaymentMethodResponse>>() {
            override fun onNext(response: BaseResponse<PaymentMethodResponse>) {
                if (response.success) {
                    val paymentMethods = response.content?.obj ?: listOf()
                    if (paymentMethods.isNotEmpty()) {
                        paymentMethodList = paymentMethods
                        updatePaymentMethodRadioGroup()
                    }
                } else {
                    Toast.makeText(this@YourActivity, "获取支付方式失败: ${response.msg}", Toast.LENGTH_SHORT).show()
                }
            }

            override fun onError(e: Throwable?) {
                super.onError(e)
                Toast.makeText(this@YourActivity, "获取支付方式失败: ${e?.message ?: "网络异常"}", Toast.LENGTH_SHORT).show()
            }
        }
    )
}

3. 动态创建RadioButton

/**
 * 更新支付方式RadioGroup
 */
private fun updatePaymentMethodRadioGroup() {
    // 清空原有RadioButton
    binding.paymentMethodRadioGroup.removeAllViews()

    // 动态添加RadioButton
    paymentMethodList.forEachIndexed { index, method ->
        val radioButton = RadioButton(this)
        radioButton.id = View.generateViewId()
        radioButton.layoutParams = LinearLayout.LayoutParams(0, resources.getDimensionPixelSize(R.dimen.dimen_40), 1.0f)

        // 设置样式
        radioButton.text = method.name
        radioButton.background = resources.getDrawable(R.drawable.radio_selector)
        radioButton.buttonDrawable = null
        radioButton.gravity = Gravity.CENTER
        radioButton.setTextColor(resources.getColorStateList(R.color.radio_button_text_color))
        radioButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)

        // 添加到RadioGroup
        binding.paymentMethodRadioGroup.addView(radioButton)

        // 默认选中第一个
        if (index == 0) {
            radioButton.isChecked = true
            paymentMethod = method.name
            paymentId = method.id
        }
    }

    // 设置选择监听
    binding.paymentMethodRadioGroup.setOnCheckedChangeListener { group, checkedId ->
        for (i in 0 until group.childCount) {
            val radioButton = group.getChildAt(i) as RadioButton
            if (radioButton.id == checkedId) {
                paymentMethod = radioButton.text.toString()
                paymentId = paymentMethodList[i].id
                break
            }
        }
    }
}

4. 布局文件配置

<RadioGroup
    android:id="@+id/paymentMethodRadioGroup"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">
    <!-- 动态添加RadioButton,不需要预定义 -->
</RadioGroup>

功能特点

  1. 动态获取: 从服务器动态获取支付方式列表,支持后台配置
  2. 自动布局: 根据支付方式数量自动调整RadioButton布局
  3. 样式统一: 所有动态创建的RadioButton使用统一的样式
  4. 默认选择: 自动选中第一个支付方式作为默认选项
  5. 事件处理: 支持选择变化监听,实时更新当前选中的支付方式

注意事项

  1. 确保在调用getPaymentMethods()前已经初始化了相关的UI组件
  2. 动态创建的RadioButton需要设置唯一的ID,使用View.generateViewId()
  3. 在Activity销毁时注意清理相关资源,避免内存泄漏
  4. 网络请求失败时要有相应的错误处理机制

注意事项

  1. 数据库迁移
  • 当修改数据库结构时,需要更新版本号
  • 提供相应的Migration策略
  1. 权限处理
  • 确保在使用相关功能前申请必要权限
  • 使用XXPermissions进行权限管理
  1. 数据安全
  • 敏感数据需要加密存储
  • 注意用户数据的安全处理
  1. 异常处理
  • 所有try catch块中必须使用CrashReport.postCatchedException(e)上报异常
  • 确保异常信息被正确记录和上报
  • 避免异常信息泄露敏感数据
  1. API异常处理最佳实践
  • 针对特定错误码提供友好的用户提示
  • 区分网络异常、业务异常和系统异常
  • 异常发生后及时重置UI状态,允许用户重试
  • 提供明确的操作指引,如"请先进行开卡操作"

API异常处理示例

// 在API回调中处理不同类型的异常
object : SubscriberListener<BaseResponse<YourDataType>>() {
    override fun onNext(response: BaseResponse<YourDataType>) {
        if (response.success) {
            // 处理成功情况
            handleSuccess(response.content)
        } else {
            // 处理业务异常
            handleBusinessError(response.code, response.msg)
        }
    }

    override fun onError(e: Throwable?) {
        super.onError(e)
        // 处理网络异常
        handleNetworkError(e)
        // 重置UI状态
        resetViewState()
    }
}

// 业务异常处理方法
private fun handleBusinessError(code: String?, msg: String?) {
    when (code) {
        "1081" -> ToastUtil.show("该卡片未在系统中注册,请先进行开卡操作")
        "1001" -> ToastUtil.show("权限不足,请联系管理员")
        "1002" -> ToastUtil.show("账户余额不足")
        else -> {
            val errorMsg = when {
                msg.isNullOrBlank() -> "操作失败,请重试"
                msg.contains("数据不存在") -> "数据不存在,请检查输入信息"
                msg.contains("网络") -> "网络连接异常,请检查网络后重试"
                msg.contains("超时") -> "请求超时,请重试"
                else -> "操作失败: $msg"
            }
            ToastUtil.show(errorMsg)
        }
    }
    // 重置界面状态
    resetViewState()
}

// 网络异常处理方法
private fun handleNetworkError(e: Throwable?) {
    val errorMsg = when {
        e?.message?.contains("timeout") == true -> "网络请求超时,请检查网络连接"
        e?.message?.contains("network") == true -> "网络连接失败,请检查网络设置"
        e?.message?.contains("host") == true -> "服务器连接失败,请稍后重试"
        else -> "网络异常: ${e?.message ?: "未知错误"}"
    }
    ToastUtil.show(errorMsg)
}

### Dialog弹窗使用最佳实践

项目中提供了多种Dialog组件,用于不同的交互场景。推荐使用项目已有的Dialog组件来保持UI风格的一致性。

#### 常用Dialog组件

1. **ConfirmDialog**: 确认对话框,用于重要操作的二次确认
2. **TipDialog**: 提示对话框,用于显示提示信息
3. **EdtDialog**: 输入对话框,用于获取用户输入
4. **自定义Dialog**: 继承Dialog类实现特定功能

#### ConfirmDialog使用示例

// 基本用法 - 只显示消息
val dialog = ConfirmDialog(context, "操作成功")
dialog.show()

// 带标题的用法
val dialog = ConfirmDialog(context, "提示", "确认要删除这条记录吗?") {
// 点击确认按钮的回调
deleteRecord()
dialog.dismiss()
}
dialog.show()

// 在异常处理中使用
private fun handleError(title: String, message: String) {
activity?.let { activity ->
val confirmDialog = ConfirmDialog(activity, title, message) {
// 点击确认后的操作
resetViewState()
}
confirmDialog.show()
}
}
```

TipDialog使用示例

// 简单提示
val tipDialog = TipDialog(context, "操作完成")
tipDialog.show()

// 带回调的提示
val tipDialog = TipDialog(context, "确认退出应用?", object : TipUtil.TipListener {
    override fun onCancle() {
        // 取消操作
    }
})
tipDialog.show()

自定义Dialog最佳实践

class CustomDialog(context: Context) : Dialog(context, R.style.ws_pay_showSelfDialog) {
    
    private lateinit var binding: DialogCustomBinding
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DialogCustomBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        // 设置对话框属性
        setupDialog()
        
        // 初始化视图
        initViews()
    }
    
    private fun setupDialog() {
        // 设置对话框宽度为屏幕宽度的85%
        window?.apply {
            val params = attributes
            params.width = (context.resources.displayMetrics.widthPixels * 0.85).toInt()
            params.height = ViewGroup.LayoutParams.WRAP_CONTENT
            params.gravity = Gravity.CENTER
            attributes = params
            setBackgroundDrawableResource(android.R.color.transparent)
        }
        
        // 设置点击外部不取消
        setCanceledOnTouchOutside(false)
    }
    
    private fun initViews() {
        binding.btnConfirm.setOnClickListener {
            // 处理确认逻辑
            dismiss()
        }
        
        binding.btnCancel.setOnClickListener {
            dismiss()
        }
    }
}

Dialog使用注意事项

  1. 内存泄漏防护: 确保在Activity销毁时关闭Dialog
    kotlin override fun onDestroy() { super.onDestroy() dialog?.dismiss() }

  2. 生命周期管理: 在Fragment中使用Dialog时注意生命周期
    kotlin // 在Fragment中安全显示Dialog activity?.let { activity -> if (!activity.isFinishing && !activity.isDestroyed) { dialog.show() } }

  3. 样式一致性: 使用项目统一的Dialog样式
    kotlin // 使用项目定义的Dialog样式 super(context, R.style.ws_pay_showSelfDialog)

  4. 用户体验优化:

  • 重要操作使用ConfirmDialog进行二次确认
  • 错误信息使用带标题的Dialog,提供清晰的错误分类
  • 长时间操作显示加载Dialog,避免用户误操作

金额格式化最佳实践

在处理金额相关的数据时,应该统一使用两位小数格式,确保显示的一致性和准确性。

金额格式化方法

// 使用String.format格式化金额为两位小数
val amount = 123.4
val formattedAmount = String.format("%.2f", amount)
// 结果: "123.40"

// 在显示时添加货币单位
binding.balanceText.text = "${formattedAmount} 元"

金额输入验证

private fun validateAmount(amountStr: String): Double? {
    if (amountStr.isEmpty()) {
        ToastUtil.show("请输入金额")
        return null
    }
    
    val amount = try {
        amountStr.toDouble()
    } catch (e: NumberFormatException) {
        ToastUtil.show("请输入有效的金额")
        return null
    }
    
    if (amount <= 0) {
        ToastUtil.show("金额必须大于0")
        return null
    }
    
    // 检查小数位数不超过2位
    val decimalPlaces = amountStr.substringAfter('.', "").length
    if (decimalPlaces > 2) {
        ToastUtil.show("金额最多保留两位小数")
        return null
    }
    
    return amount
}

金额计算和显示示例

private fun calculateAndDisplayAmounts() {
    val rechargeAmount = validateAmount(binding.rechargeAmount.text.toString()) ?: return
    val bonusAmount = validateAmount(binding.bonusAmount.text.toString()) ?: 0.0
    
    // 计算总金额
    val totalAmount = rechargeAmount + bonusAmount
    
    // 格式化显示
    val formattedRecharge = String.format("%.2f", rechargeAmount)
    val formattedBonus = String.format("%.2f", bonusAmount)
    val formattedTotal = String.format("%.2f", totalAmount)
    
    // 更新UI显示
    binding.rechargeAmountText.text = "${formattedRecharge} 元"
    binding.bonusAmountText.text = "${formattedBonus} 元"
    binding.totalAmountText.text = "${formattedTotal} 元"
}

EditText金额输入限制

在布局文件中设置金额输入的限制:

<EditText
    android:id="@+id/amountEditText"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:inputType="numberDecimal"
    android:digits="0123456789."
    android:maxLength="10"
    android:hint="请输入金额" />

TextWatcher实现小数位限制

使用TextWatcher来限制用户只能输入最多两位小数:

/**
 * 设置金额输入限制,最多保留两位小数
 */
private fun setupAmountInputLimit(editText: EditText) {
    editText.addTextChangedListener(object : TextWatcher {
        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
        
        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
        
        override fun afterTextChanged(s: Editable?) {
            val text = s.toString()
            if (text.isEmpty()) return
            
            // 检查是否包含小数点
            if (text.contains(".")) {
                val parts = text.split(".")
                if (parts.size == 2) {
                    val decimalPart = parts[1]
                    // 如果小数位超过2位,截取前两位
                    if (decimalPart.length > 2) {
                        val newText = "${parts[0]}.${decimalPart.substring(0, 2)}"
                        editText.removeTextChangedListener(this)
                        editText.setText(newText)
                        editText.setSelection(newText.length)
                        editText.addTextChangedListener(this)
                    }
                }
            }
            
            // 防止输入多个小数点
            val dotCount = text.count { it == '.' }
            if (dotCount > 1) {
                val newText = text.substring(0, text.lastIndexOf('.'))
                editText.removeTextChangedListener(this)
                editText.setText(newText)
                editText.setSelection(newText.length)
                editText.addTextChangedListener(this)
            }
            
            // 防止以小数点开头
            if (text.startsWith(".")) {
                editText.removeTextChangedListener(this)
                editText.setText("0$text")
                editText.setSelection(editText.text.length)
                editText.addTextChangedListener(this)
            }
        }
    })
}

// 在initView中调用
setupAmountInputLimit(binding.rechargeAmount)
setupAmountInputLimit(binding.bonusAmount)

金额格式化注意事项

  1. 统一格式: 所有金额显示都使用两位小数格式
  2. 输入验证: 验证用户输入的金额格式和范围
  3. 计算精度: 使用Double类型进行金额计算,避免精度丢失
  4. 显示单位: 金额显示时统一添加货币单位(如"元")
  5. 边界检查: 检查金额的最大值和最小值限制

充值接口使用说明

项目中实现了完整的充值功能,包括充值接口调用和NFC写卡操作。

充值接口详情

接口地址: /terminal/card/termRecharge
请求方式: POST
接口描述: 终端充值接口,用于创建充值订单并返回写卡所需信息

请求参数

data class RechargeRequest(
    val rechargeType: Int,           // 充值类型 (固定值: 2)
    val cardNum: String,             // 卡号
    val money: String,               // 充值金额 (格式: "300.0")
    val amount: String,              // 充值数量
    val gift: String,                // 赠送金额
    val paymentId: String,           // 支付方式ID
    val price: String,               // 单价 (格式: "0.90")
    val remarks: String,             // 备注 (默认: "充值")
    val operator: Long               // 操作员ID
)

请求示例

{
  "rechargeType": 2,
  "cardNum": "53232810100600001",
  "money": "300.0",
  "amount": "50",
  "gift": "5",
  "paymentId": "1838466162264350722",
  "price": "0.90",
  "remarks": "充值",
  "operator": 2024090516595200300
}

返回参数

data class RechargeResult(
    val projectNo: Int,              // 项目编号
    val cardNum: String,             // 卡号
    val orderNo: String,             // 订单号
    val waterPrice: Double,          // 水价
    val time: String                 // 时间
)

返回示例

{
  "code": "0001",
  "content": {
    "projectNo": 10,
    "cardNum": "53232810100600001",
    "orderNo": "2506041414250065",
    "waterPrice": 0.9,
    "time": "2025-05-08 17:31:02"
  },
  "msg": "请求成功",
  "success": true
}

充值功能实现

1. 在Activity中调用充值接口

/**
 * 调用充值接口
 */
private fun callRechargeApi(rechargeAmount: Double, bonusAmount: Double) {
    val cardNum = cardInfo?.cardNum ?: cardAddress ?: ""
    
    // 构建充值请求参数
    val params = mapOf(
        "rechargeType" to 2,
        "cardNum" to cardNum,
        "money" to String.format("%.1f", rechargeAmount),
        "amount" to String.format("%.0f", bonusAmount),
        "gift" to String.format("%.0f", bonusAmount),
        "paymentId" to paymentId.toString(),
        "price" to "0.90",
        "remarks" to "充值",
        "operator" to 2024090516595200300L
    )

    ApiManager.getInstance().requestPostLoading(
        this,
        "terminal/card/termRecharge",
        RechargeResult::class.java,
        params,
        object : SubscriberListener<BaseResponse<RechargeResult>>() {
            override fun onNext(response: BaseResponse<RechargeResult>) {
                if (response.success && response.code == "0001") {
                    // 充值成功,跳转到写卡界面
                    response.content?.let { rechargeResult ->
                        startWriteCardActivity(rechargeResult, rechargeAmount, bonusAmount)
                    }
                } else {
                    ToastUtil.show("充值失败: ${response.msg ?: "未知错误"}")
                }
            }

            override fun onError(e: Throwable?) {
                super.onError(e)
                ToastUtil.show("充值失败: ${e?.message ?: "网络异常"}")
            }
        }
    )
}

2. 跳转到写卡界面

/**
 * 启动写卡界面
 */
private fun startWriteCardActivity(rechargeResult: RechargeResult, rechargeAmount: Double, bonusAmount: Double) {
    // 创建UserCard对象用于写卡
    val userCard = UserCard().apply {
        cardInfo?.let { info ->
            userCode = info.cardNum ?: ""
            balance = ((rechargeAmount + bonusAmount) * 100).toInt() // 转换为分
        }
        
        // 设置其他必要信息
        projectCode = rechargeResult.projectNo
        waterPrice = rechargeResult.waterPrice.toFloat()
        rechargeDate = java.util.Calendar.getInstance()
    }

    // 启动写卡Activity
    val intent = Intent(this, NfcWreatActivity::class.java).apply {
        putExtra("cardType", "USER_CARD")
        putExtra("cardAddr", cardAddress)
        putExtra("operationTypeCode", CardOperationType.Recharge.code)
        putExtra("orderNumber", rechargeResult.orderNo)
        putExtra("userCard", userCard)
    }
    
    startActivity(intent)
}

3. NFC写卡处理

NfcWreatActivity中,充值操作会被自动处理:

CardOperationType.Recharge -> {
    nfcWreatHelper.writeUserDataAsync(userCard, object : NFCCallBack {
        override fun isSusses(flag: Boolean, msg: String?) {
            runOnUiThread {
                if (flag) {
                    postCardData(cardType, cardAddr)
                    ToastUtil.show("充值写卡成功!")
                } else {
                    ToastUtil.show("充值写卡失败: ${msg ?: "未知错误"}")
                }
            }
        }
    })
}

充值流程说明

  1. 用户输入: 用户在充值界面输入充值金额和赠送金额
  2. 参数验证: 验证输入的金额格式和有效性
  3. 接口调用: 调用/terminal/card/termRecharge接口创建充值订单
  4. 订单创建: 服务器创建充值订单并返回订单信息
  5. 跳转写卡: 成功后跳转到NFC写卡界面
  6. NFC写卡: 用户贴卡进行NFC写卡操作
  7. 写卡完成: 写卡成功后更新卡片余额

注意事项

  1. 金额格式: 充值金额使用一位小数格式,如"300.0"
  2. 操作员ID: 需要根据实际登录用户设置正确的操作员ID
  3. 支付方式: 支付方式ID需要从支付方式接口获取
  4. 错误处理: 充值失败时要提供明确的错误信息
  5. 写卡验证: 写卡前要验证卡号一致性
  6. 订单跟踪: 保存订单号用于后续查询和跟踪

相关数据类

// 充值请求数据类
data class RechargeRequest(
    val rechargeType: Int,
    val cardNum: String,
    val money: String,
    val amount: String,
    val gift: String,
    val paymentId: String,
    val price: String,
    val remarks: String,
    val operator: Long
)

// 充值结果数据类
data class RechargeResult(
    val projectNo: Int,
    val cardNum: String,
    val orderNo: String,
    val waterPrice: Double,
    val time: String
)

水价接口使用说明

项目中实现了水价的动态获取和管理功能,支持从服务器获取最新水价并在全局范围内使用。

水价接口详情

接口地址: /terminal/client/getWaterPrice
请求方式: GET
接口描述: 获取当前系统水价信息

返回参数

data class WaterPriceResult(
    val price: Double                // 水价
)

返回示例

{
  "code": "0001",
  "content": {
    "price": 0.9
  },
  "msg": "请求成功",
  "success": true
}

水价功能实现

1. BaseApplication中的水价存储

class BaseApplication {
    companion object {
        // 水价信息
        var waterPrice: Double = 0.0
    }
}

2. MainActivity中延时获取水价

class MainActivity : BaseNfcActivity() {
    private val handler = Handler(Looper.getMainLooper())

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ... 其他初始化代码 ...
        
        // 延时20秒后获取水价,避免影响启动性能
        handler.postDelayed({
            getWaterPrice()
        }, 20000) // 20秒延时
    }

    /**
     * 获取水价信息
     */
    private fun getWaterPrice() {
        ApiManager.getInstance().requestGetLoading(
            this,
            "terminal/client/getWaterPrice",
            WaterPriceResult::class.java,
            null,
            object : SubscriberListener<BaseResponse<WaterPriceResult>>() {
                override fun onNext(response: BaseResponse<WaterPriceResult>) {
                    if (response.success && response.code == "0001") {
                        // 获取水价成功,保存到BaseApplication
                        response.content?.let { waterPriceResult ->
                            BaseApplication.waterPrice = waterPriceResult.price
                        }
                    }
                }

                override fun onError(e: Throwable?) {
                    super.onError(e)
                    // 网络异常时不显示错误信息,避免影响用户体验
                }
            }
        )
    }

    override fun onDestroy() {
        super.onDestroy()
        // 清理Handler回调,防止内存泄漏
        handler.removeCallbacksAndMessages(null)
    }
}

3. RechargeDetailActivity中的水价检查和使用

class RechargeDetailActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ... 其他初始化代码 ...
        
        // 检查并获取水价
        checkAndGetWaterPrice()
    }

    /**
     * 检查并获取水价
     */
    private fun checkAndGetWaterPrice() {
        // 如果BaseApplication中的水价为空或为0,则重新获取
        if (BaseApplication.waterPrice <= 0.0) {
            getWaterPrice()
        }
    }

    /**
     * 获取水价信息
     */
    private fun getWaterPrice() {
        ApiManager.getInstance().requestGetLoading(
            this,
            "terminal/client/getWaterPrice",
            WaterPriceResult::class.java,
            null,
            object : SubscriberListener<BaseResponse<WaterPriceResult>>() {
                override fun onNext(response: BaseResponse<WaterPriceResult>) {
                    if (response.success && response.code == "0001") {
                        // 获取水价成功,保存到BaseApplication
                        response.content?.let { waterPriceResult ->
                            BaseApplication.waterPrice = waterPriceResult.price
                        }
                    } else {
                        // 获取水价失败,使用默认值
                        if (BaseApplication.waterPrice <= 0.0) {
                            BaseApplication.waterPrice = 0.9 // 设置默认水价
                        }
                    }
                }

                override fun onError(e: Throwable?) {
                    super.onError(e)
                    // 网络异常,使用默认值
                    if (BaseApplication.waterPrice <= 0.0) {
                        BaseApplication.waterPrice = 0.9 // 设置默认水价
                    }
                }
            }
        )
    }

    /**
     * 在充值接口中使用水价
     */
    private fun callRechargeApi(rechargeAmount: Double, bonusAmount: Double) {
        // 获取当前水价,如果为0则使用默认值
        val currentWaterPrice = if (BaseApplication.waterPrice > 0.0) {
            BaseApplication.waterPrice
        } else {
            0.9 // 默认水价
        }

        // 构建充值请求参数
        val params = mapOf(
            // ... 其他参数 ...
            "price" to String.format("%.2f", currentWaterPrice), // 使用获取到的水价
            // ... 其他参数 ...
        )

        // ... 接口调用代码 ...
    }
}

水价管理流程

  1. 应用启动: MainActivity启动后延时20秒获取水价,避免影响启动性能
  2. 全局存储: 获取到的水价存储在BaseApplication.waterPrice中
  3. 按需获取: 在需要使用水价的界面(如充值界面)检查水价是否为空
  4. 重新获取: 如果水价为空或为0,则重新调用接口获取
  5. 默认处理: 如果获取失败,使用默认水价0.9元
  6. 实时使用: 在充值等业务中使用最新的水价信息

技术特点

  1. 性能优化: 延时获取避免影响应用启动速度
  2. 全局共享: 水价信息在BaseApplication中全局共享
  3. 按需刷新: 在需要时检查并重新获取水价
  4. 容错处理: 获取失败时使用默认水价,确保业务正常进行
  5. 内存管理: 正确处理Handler回调,防止内存泄漏

使用注意事项

  1. 延时设置: MainActivity中的20秒延时可根据实际需求调整
  2. 默认水价: 默认水价0.9元可根据实际业务需求修改
  3. 错误处理: 水价获取失败时不显示错误信息,避免影响用户体验
  4. 数据格式: 水价使用Double类型存储,使用时格式化为两位小数
  5. 生命周期: 注意在Activity销毁时清理相关资源

相关数据类

// 水价结果数据类
data class WaterPriceResult(
    val price: Double                // 水价
)

贡献指南

  1. Fork 项目
  2. 创建特性分支
  3. 提交更改
  4. 推送到分支
  5. 创建Pull Request

许可证

[添加许可证信息]

联系方式

[添加联系方式]