Files
Home-Care/hss-home-service/delivery-miniapp/pages/delivery/checkin/checkin.vue
comclib 01e1034cc1 feat: 全系统优化 — 并发控制 + 冗余清理 + 数据流修复 + 全面测试
核心修复:
- 状态机加 SELECT FOR UPDATE 行锁,消除并发竞态
- hss_md_staff 加 role 列,登录从数据库读取真实角色
- 申请重复校验排除自身,全流程 20 步闭环通过
- 派单 SQL 修复 + 支付状态机过渡 + 完成服务 plan_item_id 修复

并发控制新增:
- RedisLockService (SET NX PX + Lua 安全解锁)
- RateLimiterService (Redis 滑动窗口 + API 拦截器)
- TransactionIsolationConfig (SERIALIZABLE for 支付回调)
- MqttPublisher (异步队列 + JDK TCP 探测)
- ObjectStorageService (AWS SigV4 预签名, 纯 JDK)

冗余清理:
- 删除 6 个死代码文件 (~620 行)
- hutool-all → JDK MessageDigest, 去 MapStruct, 去 jsr310
- haversine 提取到 GeoUtil, count/round 提取到 JdbcUtil
- 创建 platform layout 组件

前端修复:
- 登录页移除角色选择器, 由后端 JWT 返回
- 移除 ClientOnly 包裹, 页面正常渲染
- SPA fallback Nginx 配置修复

Docker: 运行时镜像 eclipse-temurin:17-jre-jammy (缩小 ~300MB)

文档: 新增系统实现与修复报告.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 11:48:07 +08:00

109 lines
4.4 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page">
<view class="step-card">
<text class="step-title">📍 位置确认</text>
<view v-if="location" class="info-row">
<text class="label">经度</text><text class="value">{{ location.longitude.toFixed(6) }}</text>
<text class="label">纬度</text><text class="value">{{ location.latitude.toFixed(6) }}</text>
</view>
<button v-else @click="getGPS" :loading="locating" class="btn-outline">获取当前位置</button>
</view>
<view class="step-card">
<text class="step-title">📷 现场拍照</text>
<image v-if="photoPath" :src="photoPath" class="photo" mode="aspectFill" />
<button v-else @click="takePhoto" class="btn-outline">拍摄现场照片</button>
</view>
<view class="step-card">
<text class="step-title"> 服务对象确认</text>
<switch :checked="confirmed" @change="confirmed = $event.detail.value" color="#155EEF" />
<text class="switch-label">{{ confirmed ? '已确认' : '请服务对象确认签到' }}</text>
</view>
<button @click="submitCheckIn" :disabled="!canSubmit" :loading="submitting" class="btn-primary">
{{ submitting ? '签到中...' : '确认签到' }}
</button>
</view>
</template>
<script>
import { getLocation, chooseImage, uploadEvidence, apiPost, addWatermark } from '@/common/api.js';
import { OfflineQueue } from '@/common/offline.js';
const offline = new OfflineQueue();
export default {
data() {
return {
workOrderId: 0, location: null, locating: false,
photoPath: '', photoFileId: '', confirmed: false, submitting: false
};
},
computed: {
canSubmit() { return this.location && this.photoPath && this.confirmed; }
},
onLoad(options) { this.workOrderId = parseInt(options.id) || 0; },
methods: {
async getGPS() {
this.locating = true;
try { this.location = await getLocation(); }
catch(e) { uni.showToast({ title: '获取位置失败请确认已授权GPS权限', icon: 'none', duration: 3000 }); }
finally { this.locating = false; }
},
async takePhoto() {
try {
const res = await chooseImage(1);
this.photoPath = res.tempFilePaths[0];
uni.showLoading({ title: '上传中' });
// 添加水印(时间+地点+工单号)
try { this.photoPath = await addWatermark(this.photoPath, { workOrderId: this.workOrderId, address: '服务地址' }); } catch(e) {}
const ev = await uploadEvidence(this.photoPath, 'checkin', this.workOrderId);
this.photoFileId = ev.fileKey;
uni.hideLoading();
} catch(e) {
uni.hideLoading();
uni.showToast({ title: '拍照或上传失败', icon: 'none' });
}
},
async submitCheckIn() {
this.submitting = true;
const payload = {
latitude: this.location.latitude,
longitude: this.location.longitude,
photoFileId: this.photoFileId,
patientConfirmed: this.confirmed
};
try {
const res = await apiPost('/work-orders/' + this.workOrderId + '/check-in', payload);
if (res.data.code === 200) {
uni.showToast({ title: '签到成功' });
startTrajectory(this.workOrderId);
setTimeout(() => uni.navigateBack(), 1000);
} else {
uni.showToast({ title: res.data.message, icon: 'none' });
}
} catch(e) {
offline.add({ path: '/work-orders/' + this.workOrderId + '/check-in', data: payload });
uni.showToast({ title: '网络异常,已缓存待补传', icon: 'none' });
}
this.submitting = false;
}
}
};
</script>
<style>
.page { padding: 24rpx; background: #F8F8F8; min-height: 100vh; }
.step-card { background: #fff; border-radius: 16rpx; padding: 32rpx; margin-bottom: 20rpx; }
.step-title { font-size: 32rpx; font-weight: 700; display: block; margin-bottom: 16rpx; }
.info-row { display: flex; gap: 16rpx; flex-wrap: wrap; }
.label { font-size: 26rpx; color: #999; }
.value { font-size: 26rpx; color: #333; font-family: monospace; }
.photo { width: 100%; height: 400rpx; border-radius: 12rpx; }
.switch-label { margin-left: 16rpx; font-size: 28rpx; color: #666; }
.btn-outline { border: 1px solid #155EEF; color: #155EEF; background: #fff; border-radius: 12rpx; padding: 20rpx; font-size: 28rpx; }
.btn-primary { background: #155EEF; color: #fff; border-radius: 12rpx; padding: 24rpx; font-size: 32rpx; font-weight: 600; margin-top: 32rpx; }
.btn-primary[disabled] { opacity: 0.4; }
</style>