Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03e6c9a6bb | ||
|
|
c77c32fde1 | ||
|
|
dec5ecc100 | ||
|
|
5dc0bb2ec5 | ||
|
|
c3682d1ed3 | ||
|
|
e902f2869f | ||
|
|
867cbb77be |
@@ -87,6 +87,8 @@ dependencies {
|
||||
|
||||
implementation("org.greenrobot:eventbus:3.3.1")
|
||||
|
||||
implementation("com.google.android.flexbox:flexbox:3.0.0")
|
||||
|
||||
// implementation("io.github.jeremyliao:live-event-bus-x:1.8.0")
|
||||
// implementation("com.hoho:android-usb-serial:1.3.0")
|
||||
// implementation("io.github.jeadyx:jserialport:1.5")
|
||||
|
||||
@@ -5,8 +5,11 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import com.shuwei.intelligent.shelves.utils.BootReceiver
|
||||
import com.shuwei.intelligent.shelves.utils.CrashHandler
|
||||
import com.shuwei.intelligent.shelves.utils.FileLogger
|
||||
|
||||
/**
|
||||
* @author: star
|
||||
@@ -19,10 +22,22 @@ class App : Application() {
|
||||
super.onCreate()
|
||||
app = this
|
||||
CrashHandler.init(this)
|
||||
// 启动时立即清理一次过期日志
|
||||
cleanupLogs()
|
||||
// 设备长期不断电、进程不会重新启动,运行中每24小时定期清理一次
|
||||
logCleanupHandler.postDelayed(logCleanupRunnable, LOG_CLEANUP_INTERVAL)
|
||||
val filter = IntentFilter(Intent.ACTION_BOOT_COMPLETED)
|
||||
registerReceiver(BootReceiver(), filter)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的日志文件:文件日志与崩溃日志均保留最近7天
|
||||
*/
|
||||
private fun cleanupLogs() {
|
||||
FileLogger.cleanupExpiredLogs(this)
|
||||
CrashHandler.getInstance()?.cleanupOldCrashReports()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private lateinit var app: App
|
||||
var canteenId = "0"
|
||||
@@ -36,6 +51,19 @@ class App : Application() {
|
||||
@Volatile
|
||||
private var sharedPref: SharedPreferences? = null
|
||||
|
||||
/** 日志定期清理间隔:24小时,与日志文件按天滚动对齐 */
|
||||
private const val LOG_CLEANUP_INTERVAL = 24L * 60 * 60 * 1000
|
||||
|
||||
private val logCleanupHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
/** 日志定期清理任务:执行后重新调度自身,形成循环 */
|
||||
private val logCleanupRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
getInstance().cleanupLogs()
|
||||
logCleanupHandler.postDelayed(this, LOG_CLEANUP_INTERVAL)
|
||||
}
|
||||
}
|
||||
|
||||
fun getInstance(): App {
|
||||
return app
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ object GlobalData {
|
||||
// const val LOCAL_BASE_URL = "http://192.168.1.201:14801"
|
||||
const val LOCAL_BASE_URL = "http://192.168.10.101:24801"
|
||||
const val TEST_BASE_URL = "https://dev.yixiong-tech.com:8081"
|
||||
const val PROD_BASE_URL = "https://api.dm.yixiong-tech.com:8443"
|
||||
const val PROD_BASE_URL = "https://platform-api.uat.shuziweidao.com"
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -35,8 +35,11 @@ import com.shuwei.intelligent.shelves.serial.ProtocolConstants
|
||||
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.C_TEMP_CMD
|
||||
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.START_TEMP_CTRL_CMD
|
||||
import com.shuwei.intelligent.shelves.serial.ScaleManager
|
||||
import com.shuwei.intelligent.shelves.utils.GridLayoutTool
|
||||
import com.google.android.flexbox.FlexDirection
|
||||
import com.google.android.flexbox.FlexWrap
|
||||
import com.google.android.flexbox.FlexboxLayoutManager
|
||||
import com.shuwei.intelligent.shelves.utils.IntervalExecutor
|
||||
import com.shuwei.intelligent.shelves.utils.ext.dp
|
||||
import com.shuwei.intelligent.shelves.utils.ext.gone
|
||||
import com.shuwei.intelligent.shelves.utils.ext.toJsonString
|
||||
import com.shuwei.intelligent.shelves.utils.ext.toast
|
||||
@@ -187,8 +190,8 @@ class HomeV2Activity : BaseActivity() {
|
||||
}
|
||||
val cabinet = cabinetInitResult!!
|
||||
//行列数量后端返回是反着的
|
||||
val columns = cabinet.horizontalRows
|
||||
val rows = cabinet.verticalCount
|
||||
val rows = cabinet.horizontalRows
|
||||
val columns = cabinet.verticalCount
|
||||
log("updateUI: rows=$rows, columns=$columns")
|
||||
deviceName = cabinet.deviceName
|
||||
updateLeftStatus(deviceName)
|
||||
@@ -199,10 +202,14 @@ class HomeV2Activity : BaseActivity() {
|
||||
return
|
||||
}
|
||||
list.clear()
|
||||
tempList = GridLayoutTool.initListSortV2(rows = rows, columns = columns, source = tempList)
|
||||
list.addAll(tempList)
|
||||
|
||||
val displayMetrics = resources.displayMetrics
|
||||
shelfAdapter.itemWidth = (displayMetrics.widthPixels - 24.dp) / columns - 24.dp
|
||||
shelfAdapter.itemHeight = (displayMetrics.heightPixels - 68.dp) / rows - 24.dp
|
||||
|
||||
if (binding.rvShelf.adapter == null) {
|
||||
initRecyclerView(columns)
|
||||
initRecyclerView()
|
||||
} else {
|
||||
shelfAdapter.notifyDataSetChanged()
|
||||
}
|
||||
@@ -328,8 +335,11 @@ class HomeV2Activity : BaseActivity() {
|
||||
// syncShelfGoodsToServer()
|
||||
}
|
||||
|
||||
private fun initRecyclerView(columns: Int) {
|
||||
val layoutManager = GridLayoutTool.getLayoutManagerV2(this, columns = columns)
|
||||
private fun initRecyclerView() {
|
||||
val layoutManager = FlexboxLayoutManager(this).apply {
|
||||
flexDirection = FlexDirection.COLUMN
|
||||
flexWrap = FlexWrap.WRAP
|
||||
}
|
||||
binding.rvShelf.let {
|
||||
it.layoutManager = layoutManager
|
||||
it.adapter = shelfAdapter
|
||||
@@ -443,7 +453,7 @@ class HomeV2Activity : BaseActivity() {
|
||||
if (cabinetInitResult == null) {
|
||||
return
|
||||
}
|
||||
val submitList = GridLayoutTool.submitListSortV2(list)
|
||||
val submitList = list.sortedBy { it.slotNo?.toIntOrNull() ?: 0 }
|
||||
viewModelV2.syncCabinetData(SyncBody().apply {
|
||||
// cabinetId = list[0].cabinetId
|
||||
cabinetId = App.deviceId
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.annotation.ColorRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.chad.library.adapter4.BaseQuickAdapter
|
||||
import com.chad.library.adapter4.viewholder.QuickViewHolder
|
||||
import com.google.android.flexbox.FlexboxLayoutManager
|
||||
import com.shuwei.intelligent.shelves.R
|
||||
import com.shuwei.intelligent.shelves.databinding.ListItemShelfBinding
|
||||
import com.shuwei.intelligent.shelves.model.ShelfModelV2
|
||||
@@ -17,6 +18,12 @@ import kotlin.math.abs
|
||||
class ShelfV2Adapter(list: MutableList<ShelfModelV2>) :
|
||||
BaseQuickAdapter<ShelfModelV2, ShelfV2Adapter.VH>(list) {
|
||||
|
||||
/** 每个格口 item 的高度(像素),由 Activity 动态计算 */
|
||||
var itemHeight: Int = 0
|
||||
|
||||
/** 每个格口 item 的宽度(像素),由 Activity 动态计算 */
|
||||
var itemWidth: Int = 0
|
||||
|
||||
inner class VH(val binding: ListItemShelfBinding) : QuickViewHolder(binding.root)
|
||||
|
||||
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
|
||||
@@ -26,6 +33,12 @@ class ShelfV2Adapter(list: MutableList<ShelfModelV2>) :
|
||||
|
||||
override fun onBindViewHolder(holder: VH, position: Int, item: ShelfModelV2?) {
|
||||
item ?: return
|
||||
// 动态设置 item 宽高
|
||||
val lp = holder.itemView.layoutParams as? FlexboxLayoutManager.LayoutParams
|
||||
lp?.apply {
|
||||
if (itemWidth > 0) width = itemWidth
|
||||
if (itemHeight > 0) height = itemHeight
|
||||
}
|
||||
val binding = holder.binding
|
||||
|
||||
// 格口编号
|
||||
|
||||
@@ -21,6 +21,14 @@ data class CleanPackageOption(
|
||||
val needQtyKg: BigDecimal? = null,
|
||||
/** 保质期至,格式 yyyy-MM-dd */
|
||||
val expiryDate: String? = null,
|
||||
/** 申领时间,格式 yyyy-MM-dd HH:mm:ss,仅订单有 */
|
||||
val applyTime: String? = null,
|
||||
/** 数据来源:1=净菜订单,2=食材库 */
|
||||
val source: Short? = null,
|
||||
/** 净菜类型 id,仅订单有 */
|
||||
val vegTypeId: Long? = null,
|
||||
/** 食材库 id,source=2 时有值,sync 时回传 materId */
|
||||
val materId: Long? = null,
|
||||
var isSelected: Boolean = false
|
||||
) {
|
||||
/** 将净菜包选项转换为格口记录 */
|
||||
|
||||
@@ -21,6 +21,8 @@ data class MealPackageOption(
|
||||
val needQtyKg: BigDecimal? = null,
|
||||
/** 保质期至,格式 yyyy-MM-dd(包装完成后有值) */
|
||||
val expiryDate: String? = null,
|
||||
/** 数据来源:1=净菜订单,2=餐品库 */
|
||||
val source: Short? = null,
|
||||
var isSelected: Boolean = false
|
||||
) {
|
||||
/** 将餐品净菜包选项转换为格口记录 */
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.shuwei.intelligent.shelves.model
|
||||
|
||||
/**
|
||||
* 下拉列表分页请求体
|
||||
*
|
||||
* 用于净菜包/餐品净菜包下拉接口(POST),
|
||||
* pageNum、pageSize 必填,keyword 可选:
|
||||
* - 不传 keyword:返回净菜订单列表
|
||||
* - 传 keyword:搜索食材库/餐品库
|
||||
*/
|
||||
data class PageOptionsReq(
|
||||
/** 页码,从 1 开始 */
|
||||
val pageNum: Long = 1,
|
||||
/** 每页条数 */
|
||||
val pageSize: Long = 50,
|
||||
/** 搜索关键字,不传返回订单列表,传值搜索库表 */
|
||||
val keyword: String? = null
|
||||
)
|
||||
@@ -3,6 +3,7 @@ package com.shuwei.intelligent.shelves.net
|
||||
import com.shuwei.intelligent.shelves.model.CabinetInitResult
|
||||
import com.shuwei.intelligent.shelves.model.CleanPackageOption
|
||||
import com.shuwei.intelligent.shelves.model.MealPackageOption
|
||||
import com.shuwei.intelligent.shelves.model.PageOptionsReq
|
||||
import com.shuwei.intelligent.shelves.model.SyncBody
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
@@ -24,20 +25,22 @@ interface ApiServiceV2 {
|
||||
|
||||
/**
|
||||
* 4.2.1 净菜包下拉列表(itemType=1)
|
||||
* POST + JSON Body 分页请求,data 直接为列表数组
|
||||
*/
|
||||
@GET
|
||||
@POST
|
||||
suspend fun getCleanPackageOptions(
|
||||
@Url url: String = UrlConfig.CLEAN_PACKAGE_OPTIONS,
|
||||
@Query("keyword") keyword: String? = null
|
||||
@Body body: PageOptionsReq
|
||||
): RespData<List<CleanPackageOption>>
|
||||
|
||||
/**
|
||||
* 4.2.2 餐品净菜包下拉列表(itemType=2)
|
||||
* POST + JSON Body 分页请求,data 直接为列表数组
|
||||
*/
|
||||
@GET
|
||||
@POST
|
||||
suspend fun getMealPackageOptions(
|
||||
@Url url: String = UrlConfig.MEAL_PACKAGE_OPTIONS,
|
||||
@Query("keyword") keyword: String? = null
|
||||
@Body body: PageOptionsReq
|
||||
): RespData<List<MealPackageOption>>
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.shuwei.intelligent.shelves.model.CabinetInitResult
|
||||
import com.shuwei.intelligent.shelves.model.CleanPackageOption
|
||||
import com.shuwei.intelligent.shelves.model.MealPackageOption
|
||||
import com.shuwei.intelligent.shelves.model.PageOptionsReq
|
||||
import com.shuwei.intelligent.shelves.model.SyncBody
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -47,7 +48,9 @@ class NetViewModelV2 : ViewModel() {
|
||||
viewModelScope.launch {
|
||||
_cleanPackageOptionsUiState.value = UiState.Loading
|
||||
runCatching {
|
||||
val response = apiServiceV2.getCleanPackageOptions(keyword = keyword)
|
||||
val response = apiServiceV2.getCleanPackageOptions(
|
||||
body = PageOptionsReq(pageNum = 1, pageSize = 50, keyword = keyword)
|
||||
)
|
||||
if (response.isSuccess()) {
|
||||
_cleanPackageOptionsUiState.value = UiState.Success(response)
|
||||
} else {
|
||||
@@ -64,7 +67,9 @@ class NetViewModelV2 : ViewModel() {
|
||||
viewModelScope.launch {
|
||||
_mealPackageOptionsUiState.value = UiState.Loading
|
||||
runCatching {
|
||||
val response = apiServiceV2.getMealPackageOptions(keyword = keyword)
|
||||
val response = apiServiceV2.getMealPackageOptions(
|
||||
body = PageOptionsReq(pageNum = 1, pageSize = 50, keyword = keyword)
|
||||
)
|
||||
if (response.isSuccess()) {
|
||||
_mealPackageOptionsUiState.value = UiState.Success(response)
|
||||
} else {
|
||||
|
||||
@@ -39,11 +39,19 @@ class CrashHandler private constructor(private val context: Context) :
|
||||
synchronized(CrashHandler::class.java) {
|
||||
if (instance == null) {
|
||||
instance = CrashHandler(context.applicationContext)
|
||||
// 初始化时清理过期的崩溃日志(默认保留7天)
|
||||
instance?.cleanupOldCrashReports()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
* @return 已通过 init() 初始化时返回实例,否则返回 null
|
||||
*/
|
||||
fun getInstance(): CrashHandler? = instance
|
||||
|
||||
//fun getCrashReportFiles(context: Context): Array<File> {
|
||||
// val crashDir = getCrashDir()
|
||||
// return if (crashDir.exists() && crashDir.isDirectory) {
|
||||
@@ -222,16 +230,21 @@ class CrashHandler private constructor(private val context: Context) :
|
||||
* 清理旧的崩溃日志
|
||||
*/
|
||||
fun cleanupOldCrashReports(maxAgeDays: Int = 7) {
|
||||
val crashDir = getCrashDir()
|
||||
if (!crashDir.exists() || !crashDir.isDirectory) return
|
||||
// 该方法会在 Application 启动及定时任务中调用,异常必须内部消化,避免影响主流程
|
||||
try {
|
||||
val crashDir = getCrashDir()
|
||||
if (!crashDir.exists() || !crashDir.isDirectory) return
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
val maxAgeMillis = maxAgeDays * 24 * 60 * 60 * 1000L
|
||||
val now = System.currentTimeMillis()
|
||||
val maxAgeMillis = maxAgeDays * 24 * 60 * 60 * 1000L
|
||||
|
||||
crashDir.listFiles()?.forEach { file ->
|
||||
if (file.lastModified() < now - maxAgeMillis) {
|
||||
file.delete()
|
||||
crashDir.listFiles()?.forEach { file ->
|
||||
if (file.lastModified() < now - maxAgeMillis) {
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "清理崩溃日志失败", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,37 @@ class FileLogger(private val context: Context) {
|
||||
private const val LOG_DIR = "logs"
|
||||
private const val FILE_PREFIX = "log_"
|
||||
private const val FILE_EXTENSION = ".txt"
|
||||
|
||||
/** 日志保留天数,超过该天数的日志文件在应用启动时清理 */
|
||||
private const val RETENTION_DAYS = 7L
|
||||
|
||||
/** 单个日志文件大小上限(100MB),超过后当天不再写入,防止异常刷屏撑爆存储 */
|
||||
private const val MAX_LOG_FILE_SIZE = 100L * 1024 * 1024
|
||||
|
||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
private val timeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||
|
||||
/**
|
||||
* 清理过期的日志文件,保留最近 retentionDays 天
|
||||
* 按文件最后修改时间判断,最近仍在写入的文件不会被误删
|
||||
* 建议在 Application 启动时调用一次
|
||||
* @param context 上下文,用于定位 filesDir
|
||||
* @param retentionDays 日志保留天数
|
||||
*/
|
||||
fun cleanupExpiredLogs(context: Context, retentionDays: Long = RETENTION_DAYS) {
|
||||
try {
|
||||
val cutoff = System.currentTimeMillis() - retentionDays * 24 * 60 * 60 * 1000L
|
||||
context.filesDir.listFiles { _, name ->
|
||||
name.startsWith(FILE_PREFIX) && name.endsWith(FILE_EXTENSION)
|
||||
}?.forEach { file ->
|
||||
if (file.lastModified() < cutoff) {
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,6 +74,7 @@ class FileLogger(private val context: Context) {
|
||||
|
||||
/**
|
||||
* 写入文件
|
||||
* 单文件超过大小上限(100MB)时当天不再写入,防止异常刷屏撑爆存储
|
||||
*/
|
||||
private fun writeToFile(logEntry: String) {
|
||||
// val path = File(context.filesDir, LOG_DIR)
|
||||
@@ -52,10 +82,10 @@ class FileLogger(private val context: Context) {
|
||||
// path.mkdirs()
|
||||
// }
|
||||
val fileName = getCurrentDateFileName()
|
||||
// val file = File(path, fileName)
|
||||
// if (file.exists().not()) {
|
||||
// file.createNewFile()
|
||||
// }
|
||||
val file = File(context.filesDir, fileName)
|
||||
if (file.exists() && file.length() > MAX_LOG_FILE_SIZE) {
|
||||
return
|
||||
}
|
||||
val output = context.openFileOutput(fileName, Context.MODE_APPEND)
|
||||
|
||||
val writer = BufferedWriter(OutputStreamWriter(output))
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/layoutCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="216dp"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="12dp"
|
||||
app:cardBackgroundColor="@color/bg_card_blue"
|
||||
app:cardCornerRadius="12dp"
|
||||
|
||||
Reference in New Issue
Block a user