diff --git a/src/main/java/com/budwk/app/zhgh/activity/trainSignUp/service/TrainSignUpActivityService.java b/src/main/java/com/budwk/app/zhgh/activity/trainSignUp/service/TrainSignUpActivityService.java index 09ac2f6..6a3deb5 100644 --- a/src/main/java/com/budwk/app/zhgh/activity/trainSignUp/service/TrainSignUpActivityService.java +++ b/src/main/java/com/budwk/app/zhgh/activity/trainSignUp/service/TrainSignUpActivityService.java @@ -143,6 +143,7 @@ public interface TrainSignUpActivityService extends BaseService formData; try { formData = Json.fromJsonAsList(NutMap.class, mobileColumnsValue); } catch (Exception e) { - throw new IllegalArgumentException("活动报名信息格式不正确"); + throw new IllegalArgumentException("礼品邮递地址格式不正确"); } - signUpUser.setMobileColumnsValue(formData); - dao().update(signUpUser); + syncAttendMobileColumnsValue(activity.getId(), userId, formData, true); + } + + /** + * 确保扫码点位存在对应的报名记录。 + * 用户后续继续打卡新点位时,需要有 train_sign_up_user 记录承接同步后的 mobileColumnsValue。 + * + * @param activityId 当前活动ID + * @param courseId 当前点位所属课程ID + * @param userId 当前登录用户ID + */ + private void ensureScanCodeSignUpUser(String activityId, String courseId, String userId) { + TrainSignUpUser signUpUser = dao().fetch(TrainSignUpUser.class, + Cnd.where("courseId", "=", courseId).and("userId", "=", userId)); + if (signUpUser != null) { + return; + } + signUpUser = new TrainSignUpUser(); + signUpUser.setActivityId(activityId); + signUpUser.setCourseId(courseId); + signUpUser.setUserId(userId); + signUpUser.setSignUpTime(DateUtil.date()); + signUpUser.setState(1); + dao().insert(signUpUser); + } + + /** + * 获取当前活动下用户最近一次已填写的动态表单值。 + * 用户后续继续打卡新点位时,使用这份最新数据回填到新产生的已打卡记录。 + * + * @param activityId 当前活动ID + * @param userId 当前登录用户ID + * @return 最近一次已保存的动态表单字段和值 + */ + private List getActivityLatestMobileColumnsValue(String activityId, String userId) { + List signUpUserList = dao().query(TrainSignUpUser.class, + Cnd.where("activityId", "=", activityId) + .and("userId", "=", userId) + .desc("signUpTime")); + for (TrainSignUpUser signUpUser : signUpUserList) { + if (signUpUser.getMobileColumnsValue() != null && !signUpUser.getMobileColumnsValue().isEmpty()) { + return signUpUser.getMobileColumnsValue(); + } + } + return new ArrayList<>(); + } + + /** + * 将同一份动态表单同步覆盖到当前活动下全部已打卡记录。 + * 保存表单和后续新增打卡点位都走这套逻辑,保证所有 isAttend = true 的记录保持一致。 + * + * @param activityId 当前活动ID + * @param userId 当前登录用户ID + * @param formData 需要同步的动态表单值 + * @param throwWhenNoAttendRecord true-无已打卡记录时报错;false-静默跳过 + */ + private void syncAttendMobileColumnsValue(String activityId, String userId, List formData, boolean throwWhenNoAttendRecord) { + if (formData == null || formData.isEmpty()) { + return; + } + List attendCourseList = dao().query(TrainSignUpUserCourse.class, + Cnd.where("activityId", "=", activityId) + .and("userId", "=", userId) + .and("isAttend", "=", true)); + List attendCourseIdList = attendCourseList.stream() + .map(TrainSignUpUserCourse::getCourseId) + .filter(StrUtil::isNotBlank) + .distinct() + .collect(Collectors.toList()); + if (attendCourseIdList.isEmpty()) { + if (throwWhenNoAttendRecord) { + throw new IllegalArgumentException("未找到当前活动已打卡记录"); + } + return; + } + List signUpUserList = dao().query(TrainSignUpUser.class, + Cnd.where("activityId", "=", activityId) + .and("userId", "=", userId) + .and("courseId", "in", attendCourseIdList)); + if (signUpUserList == null || signUpUserList.isEmpty()) { + if (throwWhenNoAttendRecord) { + throw new IllegalArgumentException("未找到当前活动已打卡的报名记录"); + } + return; + } + signUpUserList.forEach(item -> { + item.setMobileColumnsValue(Json.fromJsonAsList(NutMap.class, Json.toJson(formData))); + }); + dao().update(signUpUserList); } /** @@ -724,7 +798,7 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl @@ -110,16 +148,20 @@ module.exports = { watch: { dynamicData: { handler: function (newValue, oldValue) { + this.initAddressStates(newValue) this.$emit("updateDynamicData", newValue) }, - deep: true + deep: true, + immediate: true } }, data() { return { //所有的picker状态 pickerStates: {}, - fileList: [] + fileList: [], + addressStates: {}, + addressAreaList: window.trainSignUpAreaList || { province_list: {}, city_list: {}, county_list: {} } } }, methods: { @@ -136,6 +178,270 @@ module.exports = { }, async onSubmit() { + }, + /** + * 初始化地址字段的联动状态。 + * 动态表单只保存 columnValue 字符串,这里需要额外拆成省市区和详细地址,方便前端进行地区联动选择。 + * + * @param columns 当前动态字段列表 + */ + initAddressStates(columns) { + if (!columns || columns.length === 0) { + return + } + columns.forEach((column) => { + if (!this.isAddressField(column)) { + return + } + const existsState = this.addressStates[column.columnCode] + if (existsState && existsState.initializedValue === column.columnValue) { + return + } + const addressState = this.parseAddressValue(column.columnValue) + addressState.initializedValue = column.columnValue || "" + this.$set(this.addressStates, column.columnCode, addressState) + }) + }, + /** + * 判断当前动态字段是否属于收货地址类字段。 + * 目前后台没有独立的地址控件类型,因此这里按字段名称和字段编码做兼容识别。 + * + * @param column 动态字段配置 + * @return true-按地址控件渲染;false-沿用原有输入方式 + */ + isAddressField(column) { + const columnName = (column && column.columnName) ? column.columnName : "" + const columnCode = (column && column.columnCode) ? column.columnCode.toLowerCase() : "" + return ["收货地址", "邮寄地址", "礼品邮递地址", "邮递地址"].some((item) => columnName.indexOf(item) > -1) + || ["receiveaddress", "mailaddress", "shippingaddress"].some((item) => columnCode.indexOf(item) > -1) + }, + /** + * 将已保存的地址字符串拆分为省市区和详细地址。 + * 如果旧数据无法可靠拆分,就保留原始值作为兼容显示,避免历史文本地址丢失。 + * + * @param addressValue 当前字段已保存的地址字符串 + * @return 地址状态对象 + */ + parseAddressValue(addressValue) { + const emptyState = { + province: "", + city: "", + county: "", + detail: "", + displayArea: "", + areaCode: "", + legacyValue: addressValue || "", + initializedValue: addressValue || "" + } + if (!addressValue) { + return emptyState + } + const provinceMatch = this.findAreaOption(addressValue, this.addressAreaList.province_list) + if (!provinceMatch) { + return emptyState + } + let remainValue = addressValue.substring(provinceMatch.name.length) + const cityMatch = this.findAreaOption(remainValue, this.addressAreaList.city_list, provinceMatch.code.substring(0, 2)) + if (!cityMatch) { + return emptyState + } + remainValue = remainValue.substring(cityMatch.name.length) + const countyMatch = this.findAreaOption(remainValue, this.addressAreaList.county_list, cityMatch.code.substring(0, 4)) + if (!countyMatch) { + return { + province: provinceMatch.name, + city: cityMatch.name, + county: "", + detail: remainValue, + displayArea: provinceMatch.name + cityMatch.name, + areaCode: cityMatch.code, + legacyValue: "", + initializedValue: addressValue || "" + } + } + remainValue = remainValue.substring(countyMatch.name.length) + return { + province: provinceMatch.name, + city: cityMatch.name, + county: countyMatch.name, + detail: remainValue, + displayArea: provinceMatch.name + cityMatch.name + countyMatch.name, + areaCode: countyMatch.code, + legacyValue: "", + initializedValue: addressValue || "" + } + }, + /** + * 根据地址字符串前缀匹配地区代码和值。 + * + * @param addressValue 地址字符串 + * @param areaMap 地区编码映射 + * @param codePrefix 编码前缀,用于限制省/市/区层级 + * @return 匹配到的地区对象 + */ + findAreaOption(addressValue, areaMap, codePrefix) { + if (!addressValue || !areaMap) { + return null + } + let match = null + Object.keys(areaMap).forEach((code) => { + if (codePrefix && code.indexOf(codePrefix) !== 0) { + return + } + const name = areaMap[code] + if (addressValue.indexOf(name) !== 0) { + return + } + if (!match || name.length > match.name.length) { + match = { + code: code, + name: name + } + } + }) + return match + }, + /** + * 获取地址字段当前展示的省市区文本。 + * + * @param column 动态字段配置 + * @return 省市区展示文本 + */ + getAddressAreaText(column) { + const state = this.addressStates[column.columnCode] + if (!state) { + return "" + } + return state.displayArea || state.legacyValue || "" + }, + /** + * 获取地区选择器默认定位的地区编码。 + * + * @param column 动态字段配置 + * @return 地区编码 + */ + getAddressAreaCode(column) { + const state = this.addressStates[column.columnCode] + return state ? state.areaCode : "" + }, + /** + * 地址地区选择字段校验。 + * 已有历史纯文本地址时允许直接通过,避免旧数据因无法拆分省市区而被强制重填。 + * + * @param column 动态字段配置 + * @return van-field 规则数组 + */ + buildAddressAreaRules(column) { + if (!column.isRequired) { + return [] + } + return [{ + validator: () => { + const state = this.addressStates[column.columnCode] + if (!state) { + return false + } + return !!(state.displayArea || state.legacyValue) + }, + message: "请选择" + column.columnName + }] + }, + /** + * 地址详细地址字段校验。 + * 新填写的数据要求必须包含详细地址,历史纯文本地址继续兼容通过。 + * + * @param column 动态字段配置 + * @return van-field 规则数组 + */ + buildAddressDetailRules(column) { + if (!column.isRequired) { + return [] + } + return [{ + validator: () => { + const state = this.addressStates[column.columnCode] + if (!state) { + return false + } + return !!(state.detail || state.legacyValue) + }, + message: "请填写详细地址" + }] + }, + /** + * 打开地址地区选择器。 + * + * @param column 动态字段配置 + */ + openAddressPicker(column) { + this.$set(this.pickerStates, column.columnCode + "Picker", true) + }, + /** + * 关闭地址地区选择器。 + * + * @param column 动态字段配置 + */ + closeAddressPicker(column) { + this.$set(this.pickerStates, column.columnCode + "Picker", false) + }, + /** + * 地址地区确认后同步 columnValue。 + * + * @param values van-area 返回的省市区数组 + * @param column 动态字段配置 + */ + addressPickerConfirm(values, column) { + const validValues = (values || []).filter((item) => item && item.name) + const province = validValues[0] ? validValues[0].name : "" + const city = validValues[1] ? validValues[1].name : "" + const county = validValues[2] ? validValues[2].name : "" + const areaCode = validValues[2] ? validValues[2].code : (validValues[1] ? validValues[1].code : "") + const currentState = this.addressStates[column.columnCode] || {} + this.$set(this.addressStates, column.columnCode, { + province: province, + city: city, + county: county, + detail: currentState.detail || "", + displayArea: province + city + county, + areaCode: areaCode, + legacyValue: "", + initializedValue: currentState.initializedValue || "" + }) + this.syncAddressColumnValue(column) + this.closeAddressPicker(column) + }, + /** + * 详细地址输入后同步完整地址字符串。 + * + * @param column 动态字段配置 + */ + addressDetailInput(column) { + const currentState = this.addressStates[column.columnCode] || {} + this.$set(this.addressStates, column.columnCode, { + province: currentState.province || "", + city: currentState.city || "", + county: currentState.county || "", + detail: currentState.detail || "", + displayArea: currentState.displayArea || "", + areaCode: currentState.areaCode || "", + legacyValue: "", + initializedValue: currentState.initializedValue || "" + }) + this.syncAddressColumnValue(column) + }, + /** + * 将省市区和详细地址重新拼装回 columnValue。 + * 后端仍然只接收一个 columnValue 字符串,因此这里保持原有存储结构不变。 + * + * @param column 动态字段配置 + */ + syncAddressColumnValue(column) { + const state = this.addressStates[column.columnCode] + if (!state) { + return + } + const addressValue = (state.displayArea || "") + (state.detail || "") + this.$set(column, "columnValue", addressValue) }, selectFieldClick(column) { this.$set(this.pickerStates, column.columnCode + "Picker", true) @@ -144,6 +450,15 @@ module.exports = { // column.selectValues = ['1', '2', '3'] } }, + /** + * 时间picker取消后,清空当前字段值。 + * + * @param column 动态字段配置 + */ + dateTimePickerCancel(column) { + this.$set(this.pickerStates, column.columnCode + "Picker", false) + this.$set(column, "columnValue", "") + }, /** * 时间picker确认 * @param time @@ -151,14 +466,23 @@ module.exports = { */ dateTimePickerConfirm(time, column) { if (column.columnType === "DATE") { - column.columnValue = this.$moment(time).format("YYYY-MM-DD") + this.$set(column, "columnValue", this.$moment(time).format("YYYY-MM-DD")) } else if (column.columnType === "DATETIME") { - column.columnValue = this.$moment(time).format("YYYY-MM-DD HH:mm:ss") + this.$set(column, "columnValue", this.$moment(time).format("YYYY-MM-DD HH:mm:ss")) } else { //预留别的类型 - column.columnValue = this.$moment(time).format("YYYY-MM-DD HH:mm:ss") + this.$set(column, "columnValue", this.$moment(time).format("YYYY-MM-DD HH:mm:ss")) } - this.pickerStates[column.columnCode + "Picker"] = false + this.$set(this.pickerStates, column.columnCode + "Picker", false) + }, + /** + * 普通picker取消后,清空当前字段值。 + * + * @param column 动态字段配置 + */ + ordinaryPickerCancel(column) { + this.$set(this.pickerStates, column.columnCode + "Picker", false) + this.$set(column, "columnValue", "") }, /** * 普通picker确认 @@ -166,8 +490,8 @@ module.exports = { * @param column */ ordinaryPickerConfirm(value, column) { - column.columnValue = value - this.pickerStates[column.columnCode + "Picker"] = false + this.$set(column, "columnValue", value) + this.$set(this.pickerStates, column.columnCode + "Picker", false) }, /** * 文件读取 diff --git a/src/main/resources/views/platform/zhghh5/activity/trainSignUp/scanCode/index.html b/src/main/resources/views/platform/zhghh5/activity/trainSignUp/scanCode/index.html index 55711ea..bc8226f 100644 --- a/src/main/resources/views/platform/zhghh5/activity/trainSignUp/scanCode/index.html +++ b/src/main/resources/views/platform/zhghh5/activity/trainSignUp/scanCode/index.html @@ -96,9 +96,9 @@ layout("/layouts/platform_h5.html"){ @@ -127,7 +127,7 @@ layout("/layouts/platform_h5.html"){
恭喜您
当前活动已达标
-
活动报名信息补充完成
+
礼品邮递地址补充完成
@@ -136,11 +136,11 @@ layout("/layouts/platform_h5.html"){ + title="填写详细信息">
-
活动报名信息
+
详细信息
@@ -156,6 +156,7 @@ layout("/layouts/platform_h5.html"){