feat(frontend+wm-revenue): #56 网上营业厅前端(水费/报装/公告/绑定)

- 前端页面: 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 (用户绑定表)
This commit is contained in:
2026-06-15 08:42:59 +08:00
parent ba535c47e2
commit 2d0d80f6d1
8 changed files with 1274 additions and 0 deletions
@@ -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<Page<WaterBill>> 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<Map<String, Object>> billDetail(@PathVariable Long billId) {
return R.ok(billService.getBillDetail(billId));
}
@Operation(summary = "在线缴费(微信下单)")
@PostMapping("/bill/pay")
public R<Map<String, Object>> billPay(@RequestBody Map<String, Object> 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<List<Map<String, Object>>> paymentRecords(
@RequestParam String customerNo,
@RequestParam(defaultValue = "20") int limit) {
List<Map<String, Object>> 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<Map<String, Object>> installApply(@RequestBody Map<String, String> 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<Map<String, Object>> installProgress(@RequestParam String applyNo) {
return R.ok(installService.getProgress(applyNo));
}
@Operation(summary = "报装申请记录列表")
@GetMapping("/install/list")
public R<List<Map<String, Object>>> installList(
@RequestParam String phone,
@RequestParam(defaultValue = "20") int limit) {
List<Map<String, Object>> 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<Page<Announcement>> 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<Announcement> noticeDetail(@PathVariable Long id) {
return R.ok(announcementService.getDetail(id));
}
@Operation(summary = "获取当前生效公告(按区域)")
@GetMapping("/notice/active")
public R<List<Announcement>> activeNotices(
@RequestParam(required = false) String areaCode) {
return R.ok(announcementService.getActiveAnnouncements(areaCode));
}
// ========== 用户绑定 ==========
@Operation(summary = "手机号绑定")
@PostMapping("/user/bindPhone")
public R<Map<String, Object>> bindPhone(@RequestBody Map<String, String> 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<Map<String, Object>> bindCustomer(@RequestBody Map<String, String> 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<Map<String, Object>> unbind(@RequestBody Map<String, String> 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<List<Map<String, Object>>> bindings(@RequestParam String openId) {
List<Map<String, Object>> 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);
}
}
@@ -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);