|
|
@@ -0,0 +1,235 @@
|
|
|
+package com.etotem.cfc.controller.admin;
|
|
|
+
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
+import com.etotem.cfc.entity.SpecialPriceDeal;
|
|
|
+import com.etotem.cfc.service.SpecialPriceService;
|
|
|
+import com.etotem.cfc.service.WechatService;
|
|
|
+import com.etotem.cfc.util.ParamUtils;
|
|
|
+import org.springframework.web.bind.annotation.PostMapping;
|
|
|
+import org.springframework.web.bind.annotation.RequestAttribute;
|
|
|
+import org.springframework.web.bind.annotation.RequestBody;
|
|
|
+import org.springframework.web.bind.annotation.RequestMapping;
|
|
|
+import org.springframework.web.bind.annotation.RestController;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 管理端-限时特价活动管理(扫码闪购)。
|
|
|
+ * 设计见 docs/superpowers/specs/2026-10-20-limited-time-special-price-design.md
|
|
|
+ */
|
|
|
+@RestController
|
|
|
+@RequestMapping("/api/admin/special-price")
|
|
|
+public class AdminSpecialPriceController {
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private SpecialPriceService specialPriceService;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private WechatService wechatService;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 分页列表。请求体:{page, size, keyword, runStatus}
|
|
|
+ * runStatus: ACTIVE/NOT_STARTED/SOLD_OUT/ENDED(NOW() 推导)或 draft/disabled(存储状态),空=全部。
|
|
|
+ */
|
|
|
+ @PostMapping("/list")
|
|
|
+ public Result<Map<String, Object>> list(@RequestBody Map<String, Object> params,
|
|
|
+ @RequestAttribute(value = "role", required = false) String role) {
|
|
|
+ if (!"admin".equals(role)) {
|
|
|
+ return Result.error("无权限:仅管理员可管理特价活动");
|
|
|
+ }
|
|
|
+ int page = intParam(params.get("page"), 1);
|
|
|
+ int size = intParam(params.get("size"), 20);
|
|
|
+ if (page < 1) page = 1;
|
|
|
+ if (size < 1) size = 20;
|
|
|
+ if (size > 100) size = 100;
|
|
|
+ String keyword = params.get("keyword") != null ? params.get("keyword").toString() : null;
|
|
|
+ String runStatus = params.get("runStatus") != null ? params.get("runStatus").toString() : null;
|
|
|
+ return Result.success(specialPriceService.list(page, size, keyword, runStatus));
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/create")
|
|
|
+ public Result<?> create(@RequestBody Map<String, Object> params,
|
|
|
+ @RequestAttribute(value = "role", required = false) String role) {
|
|
|
+ if (!"admin".equals(role)) {
|
|
|
+ return Result.error("无权限:仅管理员可管理特价活动");
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ SpecialPriceDeal deal = new SpecialPriceDeal();
|
|
|
+ deal.setProductId(ParamUtils.getLong(params.get("productId")));
|
|
|
+ deal.setName(params.get("name") != null ? params.get("name").toString() : null);
|
|
|
+ deal.setSpecialPrice(intParam(params.get("specialPrice")));
|
|
|
+ deal.setStartTime(parseDate(params.get("startTime")));
|
|
|
+ deal.setEndTime(parseDate(params.get("endTime")));
|
|
|
+ deal.setTotalQuantity(intParam(params.get("totalQuantity")));
|
|
|
+ deal.setPerUserLimit(intParam(params.get("perUserLimit")));
|
|
|
+ Integer allowCoupon = intParam(params.get("allowCoupon"));
|
|
|
+ deal.setAllowCoupon(allowCoupon != null ? allowCoupon : 0);
|
|
|
+
|
|
|
+ if (deal.getProductId() == null) return Result.error("请选择商品");
|
|
|
+ if (deal.getName() == null || deal.getName().trim().isEmpty()) return Result.error("活动名称不能为空");
|
|
|
+ if (deal.getSpecialPrice() == null) return Result.error("特价不能为空");
|
|
|
+ if (deal.getStartTime() == null || deal.getEndTime() == null) return Result.error("开始/结束时间不能为空");
|
|
|
+ if (deal.getTotalQuantity() == null || deal.getTotalQuantity() <= 0) return Result.error("总供应量必须大于0");
|
|
|
+ if (deal.getPerUserLimit() == null || deal.getPerUserLimit() < 1) return Result.error("每人限购件数不能小于1");
|
|
|
+
|
|
|
+ return Result.success(specialPriceService.create(deal));
|
|
|
+ } catch (IllegalArgumentException | IllegalStateException e) {
|
|
|
+ return Result.error(e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/update")
|
|
|
+ public Result<?> update(@RequestBody Map<String, Object> params,
|
|
|
+ @RequestAttribute(value = "role", required = false) String role) {
|
|
|
+ if (!"admin".equals(role)) {
|
|
|
+ return Result.error("无权限:仅管理员可管理特价活动");
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ Long id = ParamUtils.getLong(params.get("id"));
|
|
|
+ if (id == null) return Result.error("活动ID不能为空");
|
|
|
+ SpecialPriceDeal deal = new SpecialPriceDeal();
|
|
|
+ deal.setId(id);
|
|
|
+ deal.setProductId(ParamUtils.getLong(params.get("productId")));
|
|
|
+ deal.setName(params.get("name") != null ? params.get("name").toString() : null);
|
|
|
+ deal.setSpecialPrice(intParam(params.get("specialPrice")));
|
|
|
+ deal.setStartTime(parseDate(params.get("startTime")));
|
|
|
+ deal.setEndTime(parseDate(params.get("endTime")));
|
|
|
+ deal.setTotalQuantity(intParam(params.get("totalQuantity")));
|
|
|
+ deal.setPerUserLimit(intParam(params.get("perUserLimit")));
|
|
|
+ deal.setAllowCoupon(intParam(params.get("allowCoupon")));
|
|
|
+
|
|
|
+ if (deal.getProductId() == null) return Result.error("请选择商品");
|
|
|
+ if (deal.getName() == null || deal.getName().trim().isEmpty()) return Result.error("活动名称不能为空");
|
|
|
+ if (deal.getSpecialPrice() == null) return Result.error("特价不能为空");
|
|
|
+ if (deal.getTotalQuantity() == null || deal.getTotalQuantity() <= 0) return Result.error("总供应量必须大于0");
|
|
|
+ if (deal.getPerUserLimit() == null || deal.getPerUserLimit() < 1) return Result.error("每人限购件数不能小于1");
|
|
|
+
|
|
|
+ specialPriceService.update(deal);
|
|
|
+ return Result.success("修改成功", null);
|
|
|
+ } catch (IllegalArgumentException | IllegalStateException e) {
|
|
|
+ return Result.error(e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 启用/停用。请求体:{id, action: enable|disable}
|
|
|
+ */
|
|
|
+ @PostMapping("/status")
|
|
|
+ public Result<Void> status(@RequestBody Map<String, Object> params,
|
|
|
+ @RequestAttribute(value = "role", required = false) String role) {
|
|
|
+ if (!"admin".equals(role)) {
|
|
|
+ return Result.error("无权限:仅管理员可管理特价活动");
|
|
|
+ }
|
|
|
+ Long id = ParamUtils.getLong(params.get("id"));
|
|
|
+ String action = params.get("action") != null ? params.get("action").toString() : null;
|
|
|
+ if (id == null) return Result.error("活动ID不能为空");
|
|
|
+ if (!"enable".equals(action) && !"disable".equals(action)) {
|
|
|
+ return Result.error("action 必须为 enable/disable");
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ specialPriceService.updateStatus(id, "enable".equals(action) ? "active" : "disabled");
|
|
|
+ return Result.success("操作成功", null);
|
|
|
+ } catch (IllegalArgumentException | IllegalStateException e) {
|
|
|
+ return Result.error(e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除(仅无特价订单时允许)。请求体:{id}
|
|
|
+ */
|
|
|
+ @PostMapping("/delete")
|
|
|
+ public Result<Void> delete(@RequestBody Map<String, Object> params,
|
|
|
+ @RequestAttribute(value = "role", required = false) String role) {
|
|
|
+ if (!"admin".equals(role)) {
|
|
|
+ return Result.error("无权限:仅管理员可管理特价活动");
|
|
|
+ }
|
|
|
+ Long id = ParamUtils.getLong(params.get("id"));
|
|
|
+ if (id == null) return Result.error("活动ID不能为空");
|
|
|
+ try {
|
|
|
+ specialPriceService.delete(id);
|
|
|
+ return Result.success("删除成功", null);
|
|
|
+ } catch (IllegalArgumentException | IllegalStateException e) {
|
|
|
+ return Result.error(e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成小程序码。请求体:{id}
|
|
|
+ * scene = "id={productId}&sp={dealId}"(≤32 字符),落地页 product-detail。
|
|
|
+ */
|
|
|
+ @PostMapping("/qrcode")
|
|
|
+ public Result<Map<String, Object>> qrcode(@RequestBody Map<String, Object> params,
|
|
|
+ @RequestAttribute(value = "role", required = false) String role) {
|
|
|
+ if (!"admin".equals(role)) {
|
|
|
+ return Result.error("无权限:仅管理员可管理特价活动");
|
|
|
+ }
|
|
|
+ Long id = ParamUtils.getLong(params.get("id"));
|
|
|
+ if (id == null) return Result.error("活动ID不能为空");
|
|
|
+ SpecialPriceDeal deal = specialPriceService.getById(id);
|
|
|
+ if (deal == null) return Result.error("特价活动不存在");
|
|
|
+
|
|
|
+ String scene = "id=" + deal.getProductId() + "&sp=" + deal.getId();
|
|
|
+ String base64 = wechatService.generateWxacode(scene,
|
|
|
+ "pages/discover-detail/product-detail/product-detail");
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ if (base64 == null || base64.isEmpty()) {
|
|
|
+ data.put("qrCodeBase64", "");
|
|
|
+ data.put("note", "测试模式无法生成真实二维码,请在生产环境操作");
|
|
|
+ } else {
|
|
|
+ data.put("qrCodeBase64", base64.startsWith("data:") ? base64 : "data:image/png;base64," + base64);
|
|
|
+ }
|
|
|
+ data.put("scene", scene);
|
|
|
+ return Result.success(data);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== helpers ====================
|
|
|
+
|
|
|
+ private static int intParam(Object value, int defaultVal) {
|
|
|
+ Integer parsed = intParam(value);
|
|
|
+ return parsed != null ? parsed : defaultVal;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Integer intParam(Object value) {
|
|
|
+ if (value == null) return null;
|
|
|
+ try {
|
|
|
+ String s = value.toString().trim();
|
|
|
+ if (s.isEmpty()) return null;
|
|
|
+ return Integer.valueOf(s);
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析时间:支持 epoch 毫秒、ISO-8601(含 T)、"yyyy-MM-dd HH:mm:ss"、"yyyy-MM-dd"。
|
|
|
+ */
|
|
|
+ private static Date parseDate(Object value) {
|
|
|
+ if (value == null) return null;
|
|
|
+ String s = value.toString().trim();
|
|
|
+ if (s.isEmpty()) return null;
|
|
|
+ try {
|
|
|
+ return new Date(Long.parseLong(s));
|
|
|
+ } catch (NumberFormatException ignored) {
|
|
|
+ }
|
|
|
+ String norm = s.replace(' ', 'T');
|
|
|
+ if (norm.indexOf('T') < 0) {
|
|
|
+ norm = norm + "T00:00:00";
|
|
|
+ } else if (norm.length() < 19) {
|
|
|
+ norm = norm + (norm.endsWith("T") ? "00:00:00" : ":00");
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return Date.from(java.time.LocalDateTime.parse(norm)
|
|
|
+ .atZone(java.time.ZoneId.systemDefault()).toInstant());
|
|
|
+ } catch (Exception e) {
|
|
|
+ try {
|
|
|
+ return Date.from(java.time.LocalDate.parse(s.trim())
|
|
|
+ .atStartOfDay(java.time.ZoneId.systemDefault()).toInstant());
|
|
|
+ } catch (Exception e2) {
|
|
|
+ throw new IllegalArgumentException("时间格式不正确: " + s);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|