左晓为主开发手持机充值管理机
README.md
@@ -232,6 +232,171 @@
- CardData: 卡片数据
- ProjectDataBean: 项目数据
## 数据类管理规范
### Bean包结构组织
项目中所有的数据类(Data Class)都应该统一放在`bean`包下的相应子目录中,按照功能和用途进行分类:
```
generallibrary/src/main/java/com/dayu/general/bean/
├── net/        # 网络接口相关数据类
├── card/       # 卡片相关数据类
├── db/         # 数据库实体类
└── ...         # 其他功能模块数据类
```
#### 1. 网络接口数据类 (bean/net/)
所有API接口的请求和响应数据类都放在`net`目录下:
```kotlin
// 请求数据类示例
data class RechargeRequest(
    val rechargeType: Int,
    val cardNum: String,
    val money: String,
    // ... 其他字段
)
// 响应数据类示例
data class CardCancelResult(
    val projectNo: Int,
    val cardNum: String,
    val orderNo: String
)
```
#### 2. 卡片相关数据类 (bean/card/)
所有卡片操作相关的数据类放在`card`目录下:
```kotlin
data class UserCard(
    var cardType: String = "",
    var balance: Int = 0,
    var userCode: String = "",
    // ... 其他字段
)
```
#### 3. 数据库实体类 (bean/db/)
所有Room数据库的实体类放在`db`目录下:
```kotlin
@Entity(tableName = "card_data")
data class CardData(
    @PrimaryKey val id: Long,
    val cardNumber: String,
    // ... 其他字段
)
```
### 数据类命名规范
1. **接口响应数据类**: 以`Result`结尾
   - `CardCancelResult` - 销卡接口响应
   - `RechargeResult` - 充值接口响应
   - `WaterPriceResult` - 水价接口响应
2. **接口请求数据类**: 以`Request`结尾
   - `RechargeRequest` - 充值接口请求
   - `SearchUserBeanRequest` - 用户搜索请求
3. **业务实体数据类**: 使用业务名称
   - `UserCard` - 用户卡片数据
   - `ClearCard` - 清零卡数据
4. **数据库实体类**: 使用表名对应的实体名
   - `CardData` - 卡片数据表
   - `ProjectDataBean` - 项目数据表
### 数据类创建最佳实践
#### 1. 文件头注释规范
```kotlin
package com.dayu.general.bean.net
/**
 * 销卡结果数据类
 * @author: zuo
 * @date: 2025/01/17
 * @description: 销卡接口返回数据
 */
data class CardCancelResult(
    val projectNo: Int,     // 项目编号
    val cardNum: String,    // 卡号
    val orderNo: String     // 订单号
)
```
#### 2. 字段注释规范
- 每个字段都应该添加行内注释说明用途
- 对于枚举值或特殊含义的字段,详细说明取值范围
#### 3. 导入使用规范
在Activity或其他类中使用数据类时,应该导入具体的数据类:
```kotlin
// 正确的导入方式
import com.dayu.general.bean.net.CardCancelResult
import com.dayu.general.bean.net.RechargeResult
// 在代码中使用
private fun handleCancelResult(result: CardCancelResult) {
    // 处理销卡结果
}
```
#### 4. 禁止内联定义
**禁止**在Activity或其他类中内联定义数据类:
```kotlin
// ❌ 错误做法 - 不要在Activity中内联定义数据类
class CardCancelActivity : BaseNfcActivity() {
    // ❌ 禁止这样做
    data class CardCancelResult(
        val projectNo: Int,
        val cardNum: String,
        val orderNo: String
    )
}
// ✅ 正确做法 - 在bean包中定义数据类
// 文件: generallibrary/src/main/java/com/dayu/general/bean/net/CardCancelResult.kt
data class CardCancelResult(
    val projectNo: Int,
    val cardNum: String,
    val orderNo: String
)
```
### 数据类管理优势
1. **统一管理**: 所有数据类集中在bean包下,便于查找和维护
2. **职责分离**: 业务逻辑和数据结构分离,代码结构更清晰
3. **复用性强**: 数据类可以在多个模块间复用
4. **易于重构**: 数据结构变更时只需修改一个文件
5. **类型安全**: 编译期类型检查,减少运行时错误
### 迁移现有代码
对于已经存在的内联数据类,应该按照以下步骤进行迁移:
1. 在bean包的相应子目录下创建数据类文件
2. 移动数据类定义到新文件
3. 在原文件中添加导入语句
4. 测试确保功能正常
5. 提交代码变更
通过统一的数据类管理规范,可以提高代码的可维护性和可读性,使项目结构更加清晰规范。
## 开发环境要求
- Android Studio Arctic Fox或更高版本
@@ -427,7 +592,7 @@
private fun getPaymentMethods() {
    ApiManager.getInstance().requestGetLoading(
        this,
        "sell/paymentmethod/get",
        "terminal/paymentmethod/get",
        PaymentMethodResponse::class.java,
        null,
        object : SubscriberListener<BaseResponse<PaymentMethodResponse>>() {
@@ -741,6 +906,589 @@
   - 错误信息使用带标题的Dialog,提供清晰的错误分类
   - 长时间操作显示加载Dialog,避免用户误操作
### 金额格式化最佳实践
在处理金额相关的数据时,应该统一使用两位小数格式,确保显示的一致性和准确性。
#### 金额格式化方法
```kotlin
// 使用String.format格式化金额为两位小数
val amount = 123.4
val formattedAmount = String.format("%.2f", amount)
// 结果: "123.40"
// 在显示时添加货币单位
binding.balanceText.text = "${formattedAmount} 元"
```
#### 金额输入验证
```kotlin
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
}
```
#### 金额计算和显示示例
```kotlin
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金额输入限制
在布局文件中设置金额输入的限制:
```xml
<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来限制用户只能输入最多两位小数:
```kotlin
/**
 * 设置金额输入限制,最多保留两位小数
 */
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
**接口描述**: 终端充值接口,用于创建充值订单并返回写卡所需信息
#### 请求参数
```kotlin
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
)
```
#### 请求示例
```json
{
  "rechargeType": 2,
  "cardNum": "53232810100600001",
  "money": "300.0",
  "amount": "50",
  "gift": "5",
  "paymentId": "1838466162264350722",
  "price": "0.90",
  "remarks": "充值",
  "operator": 2024090516595200300
}
```
#### 返回参数
```kotlin
data class RechargeResult(
    val projectNo: Int,              // 项目编号
    val cardNum: String,             // 卡号
    val orderNo: String,             // 订单号
    val waterPrice: Double,          // 水价
    val time: String                 // 时间
)
```
#### 返回示例
```json
{
  "code": "0001",
  "content": {
    "projectNo": 10,
    "cardNum": "53232810100600001",
    "orderNo": "2506041414250065",
    "waterPrice": 0.9,
    "time": "2025-05-08 17:31:02"
  },
  "msg": "请求成功",
  "success": true
}
```
### 充值功能实现
#### 1. 在Activity中调用充值接口
```kotlin
/**
 * 调用充值接口
 */
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. 跳转到写卡界面
```kotlin
/**
 * 启动写卡界面
 */
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`中,充值操作会被自动处理:
```kotlin
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. **订单跟踪**: 保存订单号用于后续查询和跟踪
### 相关数据类
```kotlin
// 充值请求数据类
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
**接口描述**: 获取当前系统水价信息
#### 返回参数
```kotlin
data class WaterPriceResult(
    val price: Double                // 水价
)
```
#### 返回示例
```json
{
  "code": "0001",
  "content": {
    "price": 0.9
  },
  "msg": "请求成功",
  "success": true
}
```
### 水价功能实现
#### 1. BaseApplication中的水价存储
```kotlin
class BaseApplication {
    companion object {
        // 水价信息
        var waterPrice: Double = 0.0
    }
}
```
#### 2. MainActivity中延时获取水价
```kotlin
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中的水价检查和使用
```kotlin
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销毁时清理相关资源
### 相关数据类
```kotlin
// 水价结果数据类
data class WaterPriceResult(
    val price: Double                // 水价
)
```
## 贡献指南
1. Fork 项目