From 2d0d80f6d11657632be4454168097a0d722fb2f9 Mon Sep 17 00:00:00 2001 From: bot_dev2 Date: Mon, 15 Jun 2026 08:42:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend+wm-revenue):=20#56=20=E7=BD=91?= =?UTF-8?q?=E4=B8=8A=E8=90=A5=E4=B8=9A=E5=8E=85=E5=89=8D=E7=AB=AF=EF=BC=88?= =?UTF-8?q?=E6=B0=B4=E8=B4=B9/=E6=8A=A5=E8=A3=85/=E5=85=AC=E5=91=8A/?= =?UTF-8?q?=E7=BB=91=E5=AE=9A=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 前端页面: WaterBillView.vue, InstallApplyView.vue, NoticeView.vue, UserBindView.vue - API 模块: wx-hall.ts (水费/报装/公告/绑定全部接口) - 路由: 注册到 frontend/src/router/index.ts (wx-hall/*) - 后端 Controller: WxHallApiController (12 端点, /api/wx-hall/*) - DDL: V6__wx_hall_user_bindng.sql (用户绑定表) --- frontend/src/api/wx-hall.ts | 85 ++++++ frontend/src/router/index.ts | 5 + .../src/views/wxhall/InstallApplyView.vue | 254 ++++++++++++++++ frontend/src/views/wxhall/NoticeView.vue | 217 +++++++++++++ frontend/src/views/wxhall/UserBindView.vue | 188 ++++++++++++ frontend/src/views/wxhall/WaterBillView.vue | 287 ++++++++++++++++++ .../wxhall/WxHallApiController.java | 220 ++++++++++++++ .../resources/db/V6__wx_hall_user_bindng.sql | 18 ++ 8 files changed, 1274 insertions(+) create mode 100644 frontend/src/api/wx-hall.ts create mode 100644 frontend/src/views/wxhall/InstallApplyView.vue create mode 100644 frontend/src/views/wxhall/NoticeView.vue create mode 100644 frontend/src/views/wxhall/UserBindView.vue create mode 100644 frontend/src/views/wxhall/WaterBillView.vue create mode 100644 wm-revenue/src/main/java/com/water/revenue/controller/wxhall/WxHallApiController.java create mode 100644 wm-revenue/src/main/resources/db/V6__wx_hall_user_bindng.sql diff --git a/frontend/src/api/wx-hall.ts b/frontend/src/api/wx-hall.ts new file mode 100644 index 00000000..78f9578a --- /dev/null +++ b/frontend/src/api/wx-hall.ts @@ -0,0 +1,85 @@ +import request from './request' + +const BASE = '/wx-hall' + +// ========== 水费查询缴费 ========== + +export function getBillList(params: { + customerNo: string + billPeriod?: string + status?: string + pageNum?: number + pageSize?: number +}) { + return request.get(`${BASE}/bill/list`, { params }) +} + +export function getBillDetail(billId: number) { + return request.get(`${BASE}/bill/${billId}`) +} + +export function payBill(data: { billId: number; amount: number }) { + return request.post(`${BASE}/bill/pay`, data) +} + +export function getPaymentRecords(params: { customerNo: string; limit?: number }) { + return request.get(`${BASE}/bill/payment-records`, { params }) +} + +// ========== 报装申请 ========== + +export function submitInstallApply(data: { + name: string + phone: string + area: string + address: string + customerType?: string + caliber?: string +}) { + return request.post(`${BASE}/install/apply`, data) +} + +export function getInstallProgress(applyNo: string) { + return request.get(`${BASE}/install/progress`, { params: { applyNo } }) +} + +export function getInstallList(params: { phone: string; limit?: number }) { + return request.get(`${BASE}/install/list`, { params }) +} + +// ========== 停水公告 ========== + +export function getNoticeList(params: { + page?: number + size?: number + type?: string + keyword?: string +}) { + return request.get(`${BASE}/notice/list`, { params }) +} + +export function getNoticeDetail(id: number) { + return request.get(`${BASE}/notice/${id}`) +} + +export function getActiveNotices(areaCode?: string) { + return request.get(`${BASE}/notice/active`, { params: { areaCode } }) +} + +// ========== 用户绑定 ========== + +export function bindPhone(data: { openId: string; phone: string }) { + return request.post(`${BASE}/user/bindPhone`, data) +} + +export function bindCustomer(data: { openId: string; customerNo: string }) { + return request.post(`${BASE}/user/bindCustomer`, data) +} + +export function unbind(data: { openId: string; bindingId: string }) { + return request.post(`${BASE}/user/unbind`, data) +} + +export function getBindings(openId: string) { + return request.get(`${BASE}/user/bindings`, { params: { openId } }) +} diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 8794d207..e7b26954 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -14,6 +14,11 @@ const routes = [ { path: 'dispatch-command', name: 'dispatchCommandList', component: () => import('@/views/dispatch-command/CommandList.vue') }, { path: 'dispatch-command/:id', name: 'dispatchCommandDetail', component: () => import('@/views/dispatch-command/CommandDetail.vue') }, { path: 'service/workbench', name: 'serviceWorkbench', component: () => import('@/views/service/CustomerServiceWorkbench.vue') }, + // 微信网厅 + { path: 'wx-hall/water-bill', name: 'wxWaterBill', component: () => import('@/views/wxhall/WaterBillView.vue') }, + { path: 'wx-hall/install-apply', name: 'wxInstallApply', component: () => import('@/views/wxhall/InstallApplyView.vue') }, + { path: 'wx-hall/notice', name: 'wxNotice', component: () => import('@/views/wxhall/NoticeView.vue') }, + { path: 'wx-hall/user-bind', name: 'wxUserBind', component: () => import('@/views/wxhall/UserBindView.vue') }, ] }, { path: '/:pathMatch(.*)*', redirect: '/dashboard' } diff --git a/frontend/src/views/wxhall/InstallApplyView.vue b/frontend/src/views/wxhall/InstallApplyView.vue new file mode 100644 index 00000000..5330c8fe --- /dev/null +++ b/frontend/src/views/wxhall/InstallApplyView.vue @@ -0,0 +1,254 @@ + + + + + diff --git a/frontend/src/views/wxhall/NoticeView.vue b/frontend/src/views/wxhall/NoticeView.vue new file mode 100644 index 00000000..181cab5b --- /dev/null +++ b/frontend/src/views/wxhall/NoticeView.vue @@ -0,0 +1,217 @@ + + + + + diff --git a/frontend/src/views/wxhall/UserBindView.vue b/frontend/src/views/wxhall/UserBindView.vue new file mode 100644 index 00000000..d41dd314 --- /dev/null +++ b/frontend/src/views/wxhall/UserBindView.vue @@ -0,0 +1,188 @@ + + + + + diff --git a/frontend/src/views/wxhall/WaterBillView.vue b/frontend/src/views/wxhall/WaterBillView.vue new file mode 100644 index 00000000..f48ae9f3 --- /dev/null +++ b/frontend/src/views/wxhall/WaterBillView.vue @@ -0,0 +1,287 @@ + + + + + diff --git a/wm-revenue/src/main/java/com/water/revenue/controller/wxhall/WxHallApiController.java b/wm-revenue/src/main/java/com/water/revenue/controller/wxhall/WxHallApiController.java new file mode 100644 index 00000000..99d44ed6 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/controller/wxhall/WxHallApiController.java @@ -0,0 +1,220 @@ +package com.water.revenue.controller.wxhall; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.water.common.core.result.R; +import com.water.revenue.entity.Announcement; +import com.water.revenue.entity.PaymentRecord; +import com.water.revenue.entity.WaterBill; +import com.water.revenue.service.AnnouncementService; +import com.water.revenue.service.BillService; +import com.water.revenue.service.InstallService; +import com.water.revenue.service.PaymentService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.*; + +/** + * 微信网厅 API(移动端适配接口) + * 所有路径前缀: /api/wx-hall/* + */ +@Slf4j +@Tag(name = "微信网厅 API") +@RestController +@RequestMapping("/wx-hall") +@RequiredArgsConstructor +public class WxHallApiController { + + private final BillService billService; + private final InstallService installService; + private final AnnouncementService announcementService; + private final PaymentService paymentService; + private final JdbcTemplate jdbcTemplate; + + // ========== 水费查询缴费 ========== + + @Operation(summary = "查询用户账单列表") + @GetMapping("/bill/list") + public R> billList( + @RequestParam String customerNo, + @RequestParam(required = false) String billPeriod, + @RequestParam(required = false) String status, + @RequestParam(defaultValue = "1") int pageNum, + @RequestParam(defaultValue = "10") int pageSize) { + return R.ok(billService.queryBills(customerNo, billPeriod, status, pageNum, pageSize)); + } + + @Operation(summary = "账单详情") + @GetMapping("/bill/{billId}") + public R> billDetail(@PathVariable Long billId) { + return R.ok(billService.getBillDetail(billId)); + } + + @Operation(summary = "在线缴费(微信下单)") + @PostMapping("/bill/pay") + public R> billPay(@RequestBody Map req) { + Long billId = Long.valueOf(req.get("billId").toString()); + BigDecimal amount = new BigDecimal(req.get("amount").toString()); + return R.ok(paymentService.payByWechat(billId, amount)); + } + + @Operation(summary = "缴费记录") + @GetMapping("/bill/payment-records") + public R>> paymentRecords( + @RequestParam String customerNo, + @RequestParam(defaultValue = "20") int limit) { + List> records = jdbcTemplate.queryForList( + "SELECT payment_no, bill_no, amount, channel, channel_name, status, pay_time, remark " + + "FROM wm_payment_record WHERE customer_no = ? ORDER BY pay_time DESC LIMIT ?", + customerNo, limit); + return R.ok(records); + } + + // ========== 报装申请 ========== + + @Operation(summary = "提交报装申请") + @PostMapping("/install/apply") + public R> installApply(@RequestBody Map req) { + return R.ok(installService.preApply( + req.get("name"), + req.get("phone"), + req.get("area"), + req.get("address"), + req.getOrDefault("customerType", "resident"), + req.getOrDefault("caliber", "DN15"))); + } + + @Operation(summary = "查询报装进度") + @GetMapping("/install/progress") + public R> installProgress(@RequestParam String applyNo) { + return R.ok(installService.getProgress(applyNo)); + } + + @Operation(summary = "报装申请记录列表") + @GetMapping("/install/list") + public R>> installList( + @RequestParam String phone, + @RequestParam(defaultValue = "20") int limit) { + List> list = jdbcTemplate.queryForList( + "SELECT application_no, applicant_name, applicant_phone, area, address, " + + "customer_type, caliber, status, created_at, updated_at " + + "FROM rev_install WHERE applicant_phone = ? ORDER BY created_at DESC LIMIT ?", + phone, limit); + return R.ok(list); + } + + // ========== 停水公告 ========== + + @Operation(summary = "公告列表") + @GetMapping("/notice/list") + public R> noticeList( + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(required = false) String type, + @RequestParam(required = false) String keyword) { + // 只返回已发布的公告 + return R.ok(announcementService.list(page, size, type, 1, keyword)); + } + + @Operation(summary = "公告详情") + @GetMapping("/notice/{id}") + public R noticeDetail(@PathVariable Long id) { + return R.ok(announcementService.getDetail(id)); + } + + @Operation(summary = "获取当前生效公告(按区域)") + @GetMapping("/notice/active") + public R> activeNotices( + @RequestParam(required = false) String areaCode) { + return R.ok(announcementService.getActiveAnnouncements(areaCode)); + } + + // ========== 用户绑定 ========== + + @Operation(summary = "手机号绑定") + @PostMapping("/user/bindPhone") + public R> bindPhone(@RequestBody Map req) { + String openId = req.get("openId"); + String phone = req.get("phone"); + if (!StringUtils.hasText(openId) || !StringUtils.hasText(phone)) { + return R.fail("openId和phone不能为空"); + } + // 检查是否已绑定 + Long existCount = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM wx_hall_user_binding WHERE open_id = ? AND phone = ? AND status = 1", + Long.class, openId, phone); + if (existCount != null && existCount > 0) { + return R.fail("该手机号已绑定"); + } + jdbcTemplate.update( + "INSERT INTO wx_hall_user_binding (open_id, phone, binding_type, status) VALUES (?, ?, 'phone', 1)", + openId, phone); + log.info("User bound phone: openId={}, phone={}", openId, phone); + return R.ok(Map.of("openId", openId, "phone", phone, "status", "bound")); + } + + @Operation(summary = "户号绑定") + @PostMapping("/user/bindCustomer") + public R> bindCustomer(@RequestBody Map req) { + String openId = req.get("openId"); + String customerNo = req.get("customerNo"); + if (!StringUtils.hasText(openId) || !StringUtils.hasText(customerNo)) { + return R.fail("openId和customerNo不能为空"); + } + // 检查是否已绑定 + Long existCount = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM wx_hall_user_binding WHERE open_id = ? AND customer_no = ? AND status = 1", + Long.class, openId, customerNo); + if (existCount != null && existCount > 0) { + return R.fail("该户号已绑定"); + } + // 查询客户名称 + String customerName = null; + try { + customerName = jdbcTemplate.queryForObject( + "SELECT customer_name FROM rev_customer WHERE customer_no = ?", + String.class, customerNo); + } catch (Exception e) { + log.warn("Customer not found: {}", customerNo); + } + jdbcTemplate.update( + "INSERT INTO wx_hall_user_binding (open_id, customer_no, customer_name, binding_type, status) VALUES (?, ?, ?, 'customer_no', 1)", + openId, customerNo, customerName); + log.info("User bound customer: openId={}, customerNo={}", openId, customerNo); + return R.ok(Map.of("openId", openId, "customerNo", customerNo, + "customerName", customerName != null ? customerName : "", "status", "bound")); + } + + @Operation(summary = "解绑") + @PostMapping("/user/unbind") + public R> unbind(@RequestBody Map req) { + String openId = req.get("openId"); + String bindingId = req.get("bindingId"); + if (!StringUtils.hasText(openId) || !StringUtils.hasText(bindingId)) { + return R.fail("参数不完整"); + } + jdbcTemplate.update( + "UPDATE wx_hall_user_binding SET status = 0, updated_at = NOW() WHERE id = ? AND open_id = ?", + Long.valueOf(bindingId), openId); + log.info("User unbound: openId={}, bindingId={}", openId, bindingId); + return R.ok(Map.of("status", "unbound")); + } + + @Operation(summary = "查询用户绑定列表") + @GetMapping("/user/bindings") + public R>> bindings(@RequestParam String openId) { + List> list = jdbcTemplate.queryForList( + "SELECT id, phone, customer_no, customer_name, binding_type, status, created_at " + + "FROM wx_hall_user_binding WHERE open_id = ? AND status = 1 ORDER BY created_at DESC", + openId); + return R.ok(list); + } +} diff --git a/wm-revenue/src/main/resources/db/V6__wx_hall_user_bindng.sql b/wm-revenue/src/main/resources/db/V6__wx_hall_user_bindng.sql new file mode 100644 index 00000000..620969b1 --- /dev/null +++ b/wm-revenue/src/main/resources/db/V6__wx_hall_user_bindng.sql @@ -0,0 +1,18 @@ +-- 微信网厅用户绑定表(手机号/户号绑定) + +CREATE TABLE IF NOT EXISTS wx_hall_user_binding ( + id BIGSERIAL PRIMARY KEY, + open_id VARCHAR(64) NOT NULL, + phone VARCHAR(20), + customer_no VARCHAR(32), + customer_name VARCHAR(100), + binding_type VARCHAR(20) NOT NULL DEFAULT 'phone', -- phone / customer_no + status INTEGER NOT NULL DEFAULT 1, -- 1-有效 0-已解绑 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_wx_binding_open_id ON wx_hall_user_binding(open_id); +CREATE INDEX IF NOT EXISTS idx_wx_binding_phone ON wx_hall_user_binding(phone); +CREATE INDEX IF NOT EXISTS idx_wx_binding_customer ON wx_hall_user_binding(customer_no); +CREATE INDEX IF NOT EXISTS idx_wx_binding_status ON wx_hall_user_binding(status);