1
This commit is contained in:
8
backend/.env
Normal file
8
backend/.env
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
WECHAT_MP_APP_ID=wx8c514804683e4be4
|
||||
WECHAT_MP_APP_SECRET=bd5f31d747b6a2c99eefecf3c8667899
|
||||
WECHAT_MP_TOKEN_CACHE_SECONDS=6900
|
||||
JWT_SECRET=U6d2lJ7lKv2PmthSxh8trE8Xl3nZfaErAgHc+X08rYs=
|
||||
DB_USER=root
|
||||
DB_PASSWORD=TONA1234
|
||||
DB_URL=jdbc:mysql://mysql.tonaspace.com:3306/partsinquiry?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8mb4&connectionCollation=utf8mb4_0900_ai_ci
|
||||
8
backend/.env.example
Normal file
8
backend/.env.example
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
WECHAT_MP_APP_ID=wx8c514804683e4be4
|
||||
WECHAT_MP_APP_SECRET=bd5f31d747b6a2c99eefecf3c8667899
|
||||
WECHAT_MP_TOKEN_CACHE_SECONDS=6900
|
||||
JWT_SECRET=U6d2lJ7lKv2PmthSxh8trE8Xl3nZfaErAgHc+X08rYs=
|
||||
spring.datasource.username=${DB_USER:root}
|
||||
spring.datasource.password=${DB_PASSWORD:TONA1234}
|
||||
spring.datasource.url=${DB_URL:jdbc:mysql://mysql.tonaspace.com:3306/partsinquiry?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8mb4&connectionCollation=utf8mb4_0900_ai_ci}
|
||||
@@ -59,6 +59,27 @@
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 认证:JWT 签发 -->
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
<artifactId>java-jwt</artifactId>
|
||||
<version>4.4.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 密码哈希:BCrypt -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-crypto</artifactId>
|
||||
<version>6.3.4</version>
|
||||
</dependency>
|
||||
|
||||
<!-- .env loader for Spring Boot -->
|
||||
<dependency>
|
||||
<groupId>me.paulschwarz</groupId>
|
||||
<artifactId>spring-dotenv</artifactId>
|
||||
<version>4.0.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.example.demo.admin;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/consults")
|
||||
public class AdminConsultController {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public AdminConsultController(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<?> list(@RequestParam(name = "shopId", required = false) Long shopId,
|
||||
@RequestParam(name = "status", required = false) String status,
|
||||
@RequestParam(name = "kw", required = false) String kw,
|
||||
@RequestParam(name = "page", defaultValue = "1") int page,
|
||||
@RequestParam(name = "size", defaultValue = "20") int size) {
|
||||
int offset = Math.max(0, page - 1) * Math.max(1, size);
|
||||
StringBuilder sql = new StringBuilder(
|
||||
"SELECT c.id,c.user_id AS userId,c.shop_id AS shopId,s.name AS shopName,c.topic,c.message,c.status,c.created_at " +
|
||||
"FROM consults c JOIN shops s ON s.id=c.shop_id WHERE 1=1");
|
||||
List<Object> ps = new ArrayList<>();
|
||||
if (shopId != null) { sql.append(" AND c.shop_id=?"); ps.add(shopId); }
|
||||
if (status != null && !status.isBlank()) { sql.append(" AND c.status=?"); ps.add(status); }
|
||||
if (kw != null && !kw.isBlank()) {
|
||||
sql.append(" AND (c.topic LIKE ? OR c.message LIKE ?)"); String like = "%"+kw.trim()+"%"; ps.add(like); ps.add(like);
|
||||
}
|
||||
sql.append(" ORDER BY c.id DESC LIMIT ").append(offset).append(", ").append(size);
|
||||
List<Map<String,Object>> list = jdbcTemplate.query(sql.toString(), ps.toArray(), (rs, i) -> {
|
||||
Map<String,Object> m = new LinkedHashMap<>();
|
||||
m.put("id", rs.getLong("id"));
|
||||
m.put("userId", rs.getLong("userId"));
|
||||
m.put("shopId", rs.getLong("shopId"));
|
||||
m.put("shopName", rs.getString("shopName"));
|
||||
m.put("topic", rs.getString("topic"));
|
||||
m.put("message", rs.getString("message"));
|
||||
m.put("status", rs.getString("status"));
|
||||
m.put("createdAt", rs.getTimestamp("created_at"));
|
||||
return m;
|
||||
});
|
||||
Map<String,Object> body = new HashMap<>(); body.put("list", list); return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/reply")
|
||||
public ResponseEntity<?> reply(@PathVariable("id") Long id,
|
||||
@RequestHeader(name = "X-User-Id") Long userId,
|
||||
@RequestBody Map<String,Object> body) {
|
||||
String content = body == null ? null : String.valueOf(body.get("content"));
|
||||
if (content == null || content.isBlank()) return ResponseEntity.badRequest().body(Map.of("message","content required"));
|
||||
jdbcTemplate.update("INSERT INTO consult_replies (consult_id,user_id,content,created_at) VALUES (?,?,?,NOW())", id, userId, content);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/resolve")
|
||||
public ResponseEntity<?> resolve(@PathVariable("id") Long id) {
|
||||
jdbcTemplate.update("UPDATE consults SET status='resolved', updated_at=NOW() WHERE id=?", id);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.example.demo.admin;
|
||||
|
||||
import com.example.demo.common.AppDefaultsProperties;
|
||||
import com.example.demo.product.entity.ProductCategory;
|
||||
import com.example.demo.product.entity.ProductUnit;
|
||||
import com.example.demo.product.repo.CategoryRepository;
|
||||
import com.example.demo.product.repo.UnitRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/dicts")
|
||||
public class AdminDictController {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final UnitRepository unitRepository;
|
||||
private final CategoryRepository categoryRepository;
|
||||
private final AppDefaultsProperties defaults;
|
||||
|
||||
public AdminDictController(JdbcTemplate jdbcTemplate, UnitRepository unitRepository, CategoryRepository categoryRepository, AppDefaultsProperties defaults) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.unitRepository = unitRepository;
|
||||
this.categoryRepository = categoryRepository;
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
private boolean isPlatformAdmin(Long userId) {
|
||||
Integer admin = jdbcTemplate.queryForObject("SELECT is_platform_admin FROM users WHERE id=? LIMIT 1", Integer.class, userId);
|
||||
return admin != null && admin == 1;
|
||||
}
|
||||
|
||||
// ===== Units =====
|
||||
@PostMapping("/units")
|
||||
@Transactional
|
||||
public ResponseEntity<?> createUnit(@RequestHeader("X-User-Id") Long userId, @RequestBody Map<String,Object> body) {
|
||||
if (!isPlatformAdmin(userId)) return ResponseEntity.status(403).body(Map.of("message","forbidden"));
|
||||
String name = body == null ? null : (String) body.get("name");
|
||||
if (name == null || name.isBlank()) return ResponseEntity.badRequest().body(Map.of("message","name required"));
|
||||
Long sid = defaults.getDictShopId();
|
||||
if (unitRepository.existsByShopIdAndName(sid, name)) return ResponseEntity.badRequest().body(Map.of("message","名称已存在"));
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
ProductUnit u = new ProductUnit();
|
||||
u.setShopId(sid);
|
||||
u.setUserId(userId);
|
||||
u.setName(name.trim());
|
||||
u.setCreatedAt(now);
|
||||
u.setUpdatedAt(now);
|
||||
unitRepository.save(u);
|
||||
return ResponseEntity.ok(Map.of("id", u.getId()));
|
||||
}
|
||||
|
||||
@PutMapping("/units/{id}")
|
||||
@Transactional
|
||||
public ResponseEntity<?> updateUnit(@RequestHeader("X-User-Id") Long userId, @PathVariable("id") Long id, @RequestBody Map<String,Object> body) {
|
||||
if (!isPlatformAdmin(userId)) return ResponseEntity.status(403).body(Map.of("message","forbidden"));
|
||||
String name = body == null ? null : (String) body.get("name");
|
||||
if (name == null || name.isBlank()) return ResponseEntity.badRequest().body(Map.of("message","name required"));
|
||||
ProductUnit u = unitRepository.findById(id).orElse(null);
|
||||
if (u == null || !u.getShopId().equals(defaults.getDictShopId())) return ResponseEntity.status(404).body(Map.of("message","not found"));
|
||||
if (!u.getName().equals(name.trim()) && unitRepository.existsByShopIdAndName(defaults.getDictShopId(), name.trim()))
|
||||
return ResponseEntity.badRequest().body(Map.of("message","名称已存在"));
|
||||
u.setUserId(userId);
|
||||
u.setName(name.trim());
|
||||
u.setUpdatedAt(LocalDateTime.now());
|
||||
unitRepository.save(u);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@DeleteMapping("/units/{id}")
|
||||
@Transactional
|
||||
public ResponseEntity<?> deleteUnit(@RequestHeader("X-User-Id") Long userId, @PathVariable("id") Long id) {
|
||||
if (!isPlatformAdmin(userId)) return ResponseEntity.status(403).body(Map.of("message","forbidden"));
|
||||
ProductUnit u = unitRepository.findById(id).orElse(null);
|
||||
if (u == null || !u.getShopId().equals(defaults.getDictShopId())) return ResponseEntity.status(404).body(Map.of("message","not found"));
|
||||
// 引用保护:若有商品使用该单位,阻止删除
|
||||
Long cnt = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM products WHERE unit_id=?", Long.class, id);
|
||||
if (cnt != null && cnt > 0) {
|
||||
return ResponseEntity.status(409).body(Map.of("message","存在引用,无法删除"));
|
||||
}
|
||||
unitRepository.deleteById(id);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
// ===== Categories =====
|
||||
@PostMapping("/categories")
|
||||
@Transactional
|
||||
public ResponseEntity<?> createCategory(@RequestHeader("X-User-Id") Long userId, @RequestBody Map<String,Object> body) {
|
||||
if (!isPlatformAdmin(userId)) return ResponseEntity.status(403).body(Map.of("message","forbidden"));
|
||||
String name = body == null ? null : (String) body.get("name");
|
||||
if (name == null || name.isBlank()) return ResponseEntity.badRequest().body(Map.of("message","name required"));
|
||||
Long sid = defaults.getDictShopId();
|
||||
if (categoryRepository.existsByShopIdAndName(sid, name)) return ResponseEntity.badRequest().body(Map.of("message","名称已存在"));
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
ProductCategory c = new ProductCategory();
|
||||
c.setShopId(sid);
|
||||
c.setUserId(userId);
|
||||
c.setName(name.trim());
|
||||
c.setSortOrder(0);
|
||||
c.setCreatedAt(now);
|
||||
c.setUpdatedAt(now);
|
||||
categoryRepository.save(c);
|
||||
return ResponseEntity.ok(Map.of("id", c.getId()));
|
||||
}
|
||||
|
||||
@PutMapping("/categories/{id}")
|
||||
@Transactional
|
||||
public ResponseEntity<?> updateCategory(@RequestHeader("X-User-Id") Long userId, @PathVariable("id") Long id, @RequestBody Map<String,Object> body) {
|
||||
if (!isPlatformAdmin(userId)) return ResponseEntity.status(403).body(Map.of("message","forbidden"));
|
||||
String name = body == null ? null : (String) body.get("name");
|
||||
if (name == null || name.isBlank()) return ResponseEntity.badRequest().body(Map.of("message","name required"));
|
||||
ProductCategory c = categoryRepository.findById(id).orElse(null);
|
||||
if (c == null || !c.getShopId().equals(defaults.getDictShopId())) return ResponseEntity.status(404).body(Map.of("message","not found"));
|
||||
if (!c.getName().equals(name.trim()) && categoryRepository.existsByShopIdAndName(defaults.getDictShopId(), name.trim()))
|
||||
return ResponseEntity.badRequest().body(Map.of("message","名称已存在"));
|
||||
c.setUserId(userId);
|
||||
c.setName(name.trim());
|
||||
c.setUpdatedAt(LocalDateTime.now());
|
||||
categoryRepository.save(c);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@DeleteMapping("/categories/{id}")
|
||||
@Transactional
|
||||
public ResponseEntity<?> deleteCategory(@RequestHeader("X-User-Id") Long userId, @PathVariable("id") Long id) {
|
||||
if (!isPlatformAdmin(userId)) return ResponseEntity.status(403).body(Map.of("message","forbidden"));
|
||||
ProductCategory c = categoryRepository.findById(id).orElse(null);
|
||||
if (c == null || !c.getShopId().equals(defaults.getDictShopId())) return ResponseEntity.status(404).body(Map.of("message","not found"));
|
||||
// 子类与引用保护
|
||||
Long child = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM product_categories WHERE parent_id=?", Long.class, id);
|
||||
if (child != null && child > 0) return ResponseEntity.status(409).body(Map.of("message","存在子类,无法删除"));
|
||||
Long cnt = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM products WHERE category_id=?", Long.class, id);
|
||||
if (cnt != null && cnt > 0) return ResponseEntity.status(409).body(Map.of("message","存在引用,无法删除"));
|
||||
categoryRepository.deleteById(id);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.example.demo.admin;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/parts")
|
||||
public class AdminPartController {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public AdminPartController(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<?> list(@RequestParam(name = "shopId", required = false) Long shopId,
|
||||
@RequestParam(name = "kw", required = false) String kw,
|
||||
@RequestParam(name = "status", required = false) String status,
|
||||
@RequestParam(name = "page", defaultValue = "1") int page,
|
||||
@RequestParam(name = "size", defaultValue = "20") int size) {
|
||||
int offset = Math.max(0, page - 1) * Math.max(1, size);
|
||||
StringBuilder sql = new StringBuilder(
|
||||
"SELECT p.id,p.user_id AS userId,p.shop_id AS shopId,s.name AS shopName,p.name,p.brand,p.model,p.spec,p.is_blacklisted " +
|
||||
"FROM products p JOIN shops s ON s.id=p.shop_id WHERE p.deleted_at IS NULL");
|
||||
List<Object> ps = new ArrayList<>();
|
||||
if (shopId != null) { sql.append(" AND p.shop_id=?"); ps.add(shopId); }
|
||||
if (kw != null && !kw.isBlank()) {
|
||||
sql.append(" AND (p.name LIKE ? OR p.brand LIKE ? OR p.model LIKE ? OR p.spec LIKE ?)");
|
||||
String like = "%" + kw.trim() + "%";
|
||||
ps.add(like); ps.add(like); ps.add(like); ps.add(like);
|
||||
}
|
||||
if (status != null && !status.isBlank()) {
|
||||
// 支持两种入参:"1/0"(正常/黑名单)或历史字符串(忽略过滤避免报错)
|
||||
try {
|
||||
int s = Integer.parseInt(status);
|
||||
int isBlack = (s == 1 ? 0 : 1);
|
||||
sql.append(" AND p.is_blacklisted=?");
|
||||
ps.add(isBlack);
|
||||
} catch (NumberFormatException ignore) {
|
||||
// 兼容历史:pending/approved/rejected → 不加过滤
|
||||
}
|
||||
}
|
||||
sql.append(" ORDER BY p.id DESC LIMIT ").append(offset).append(", ").append(size);
|
||||
List<Map<String,Object>> list = jdbcTemplate.query(sql.toString(), ps.toArray(), (rs, i) -> {
|
||||
Map<String,Object> m = new LinkedHashMap<>();
|
||||
m.put("id", rs.getLong("id"));
|
||||
m.put("userId", rs.getLong("userId"));
|
||||
m.put("shopId", rs.getLong("shopId"));
|
||||
m.put("shopName", rs.getString("shopName"));
|
||||
m.put("name", rs.getString("name"));
|
||||
m.put("brand", rs.getString("brand"));
|
||||
m.put("model", rs.getString("model"));
|
||||
m.put("spec", rs.getString("spec"));
|
||||
m.put("status", rs.getInt("is_blacklisted") == 1 ? 0 : 1);
|
||||
return m;
|
||||
});
|
||||
Map<String,Object> body = new HashMap<>(); body.put("list", list); return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/blacklist")
|
||||
public ResponseEntity<?> blacklist(@PathVariable("id") Long id) {
|
||||
jdbcTemplate.update("UPDATE products SET is_blacklisted=1 WHERE id=?", id);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/restore")
|
||||
public ResponseEntity<?> restore(@PathVariable("id") Long id) {
|
||||
jdbcTemplate.update("UPDATE products SET is_blacklisted=0 WHERE id=?", id);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.example.demo.admin;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/users")
|
||||
public class AdminUserController {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public AdminUserController(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<?> list(@RequestParam(name = "shopId", required = false) Long shopId,
|
||||
@RequestParam(name = "kw", required = false) String kw,
|
||||
@RequestParam(name = "page", defaultValue = "1") int page,
|
||||
@RequestParam(name = "size", defaultValue = "20") int size) {
|
||||
int offset = Math.max(0, page - 1) * Math.max(1, size);
|
||||
StringBuilder sql = new StringBuilder(
|
||||
"SELECT u.id,u.name,u.phone,u.role,u.status,u.is_owner AS isOwner,u.shop_id AS shopId,s.name AS shopName " +
|
||||
"FROM users u JOIN shops s ON s.id=u.shop_id WHERE 1=1");
|
||||
List<Object> ps = new ArrayList<>();
|
||||
if (shopId != null) { sql.append(" AND u.shop_id=?"); ps.add(shopId); }
|
||||
if (kw != null && !kw.isBlank()) {
|
||||
sql.append(" AND (u.name LIKE ? OR u.phone LIKE ? OR u.role LIKE ?)");
|
||||
String like = "%" + kw.trim() + "%";
|
||||
ps.add(like); ps.add(like); ps.add(like);
|
||||
}
|
||||
sql.append(" ORDER BY id DESC LIMIT ? OFFSET ?");
|
||||
ps.add(size); ps.add(offset);
|
||||
List<Map<String,Object>> list = jdbcTemplate.query(sql.toString(), ps.toArray(), (rs, i) -> {
|
||||
Map<String,Object> m = new LinkedHashMap<>();
|
||||
m.put("id", rs.getLong("id"));
|
||||
m.put("name", rs.getString("name"));
|
||||
m.put("phone", rs.getString("phone"));
|
||||
m.put("role", rs.getString("role"));
|
||||
m.put("status", rs.getInt("status"));
|
||||
m.put("isOwner", rs.getBoolean("isOwner"));
|
||||
m.put("shopId", rs.getLong("shopId"));
|
||||
m.put("shopName", rs.getString("shopName"));
|
||||
return m;
|
||||
});
|
||||
Map<String,Object> body = new HashMap<>();
|
||||
body.put("list", list);
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<?> update(@PathVariable("id") Long id,
|
||||
@RequestBody Map<String,Object> body) {
|
||||
// 仅允许更新以下字段
|
||||
String name = optString(body.get("name"));
|
||||
String phone = optString(body.get("phone"));
|
||||
String role = optString(body.get("role"));
|
||||
Integer status = optInteger(body.get("status"));
|
||||
Boolean isOwner = optBoolean(body.get("isOwner"));
|
||||
|
||||
List<String> sets = new ArrayList<>();
|
||||
List<Object> ps = new ArrayList<>();
|
||||
if (name != null) { sets.add("name=?"); ps.add(name); }
|
||||
if (phone != null) { sets.add("phone=?"); ps.add(phone); }
|
||||
if (role != null) { sets.add("role=?"); ps.add(role); }
|
||||
if (status != null) { sets.add("status=?"); ps.add(status); }
|
||||
if (isOwner != null) { sets.add("is_owner=?"); ps.add(isOwner ? 1 : 0); }
|
||||
if (sets.isEmpty()) return ResponseEntity.ok().build();
|
||||
String sql = "UPDATE users SET " + String.join(",", sets) + " WHERE id=?";
|
||||
ps.add(id);
|
||||
jdbcTemplate.update(sql, ps.toArray());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
private static String optString(Object v) { return (v == null ? null : String.valueOf(v)); }
|
||||
private static Integer optInteger(Object v) { try { return v==null?null:Integer.valueOf(String.valueOf(v)); } catch (Exception e) { return null; } }
|
||||
private static Boolean optBoolean(Object v) { if (v==null) return null; String s=String.valueOf(v); return ("1".equals(s) || "true".equalsIgnoreCase(s)); }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.example.demo.admin;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/vips")
|
||||
public class AdminVipController {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public AdminVipController(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<?> list(@RequestParam(name = "shopId", required = false) Long shopId,
|
||||
@RequestParam(name = "phone", required = false) String phone,
|
||||
@RequestParam(name = "status", required = false) Integer status,
|
||||
@RequestParam(name = "page", defaultValue = "1") int page,
|
||||
@RequestParam(name = "size", defaultValue = "20") int size) {
|
||||
int offset = Math.max(0, page - 1) * Math.max(1, size);
|
||||
StringBuilder sql = new StringBuilder(
|
||||
"SELECT v.id,v.user_id AS userId,v.is_vip AS isVip,v.status,v.expire_at AS expireAt,v.shop_id AS shopId,s.name AS shopName,u.name,u.phone " +
|
||||
"FROM vip_users v JOIN users u ON u.id=v.user_id JOIN shops s ON s.id=v.shop_id WHERE 1=1");
|
||||
List<Object> ps = new ArrayList<>();
|
||||
if (shopId != null) { sql.append(" AND v.shop_id=?"); ps.add(shopId); }
|
||||
if (phone != null && !phone.isBlank()) { sql.append(" AND u.phone LIKE ?"); ps.add("%"+phone.trim()+"%"); }
|
||||
if (status != null) { sql.append(" AND v.status = ?"); ps.add(status); }
|
||||
sql.append(" ORDER BY v.id DESC LIMIT ").append(offset).append(", ").append(size);
|
||||
List<Map<String,Object>> list = jdbcTemplate.query(sql.toString(), ps.toArray(), (rs, i) -> {
|
||||
Map<String,Object> m = new LinkedHashMap<>();
|
||||
m.put("id", rs.getLong("id"));
|
||||
m.put("userId", rs.getLong("userId"));
|
||||
m.put("isVip", rs.getInt("isVip"));
|
||||
m.put("status", rs.getInt("status"));
|
||||
m.put("expireAt", rs.getTimestamp("expireAt"));
|
||||
m.put("shopId", rs.getLong("shopId"));
|
||||
m.put("shopName", rs.getString("shopName"));
|
||||
m.put("name", rs.getString("name"));
|
||||
m.put("phone", rs.getString("phone"));
|
||||
return m;
|
||||
});
|
||||
return ResponseEntity.ok(Map.of("list", list));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<?> create(@RequestHeader(name = "X-User-Id") Long userId,
|
||||
@RequestBody Map<String,Object> body) {
|
||||
Long sid = asLong(body.get("shopId"));
|
||||
if (sid == null) return ResponseEntity.badRequest().body(Map.of("message","shopId required"));
|
||||
Long uid = asLong(body.get("userId")); if (uid == null) return ResponseEntity.badRequest().body(Map.of("message","userId required"));
|
||||
Integer isVip = asIntOr(body.get("isVip"), 1);
|
||||
java.sql.Timestamp expireAt = asTimestamp(body.get("expireAt"));
|
||||
String remark = str(body.get("remark"));
|
||||
jdbcTemplate.update("INSERT INTO vip_users (shop_id,user_id,is_vip,status,expire_at,remark,created_at,updated_at) VALUES (?,?,?,?,?, ?,NOW(),NOW())",
|
||||
sid, uid, isVip, 0, expireAt, remark);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<?> update(@PathVariable("id") Long id,
|
||||
@RequestBody Map<String,Object> body) {
|
||||
List<String> sets = new ArrayList<>(); List<Object> ps = new ArrayList<>();
|
||||
Integer isVip = asInt(body.get("isVip")); if (isVip != null) { sets.add("is_vip=?"); ps.add(isVip); }
|
||||
Integer status = asInt(body.get("status")); if (status != null) { sets.add("status=?"); ps.add(status); }
|
||||
java.sql.Timestamp expireAt = asTimestamp(body.get("expireAt")); if (expireAt != null) { sets.add("expire_at=?"); ps.add(expireAt); }
|
||||
String remark = str(body.get("remark")); if (remark != null) { sets.add("remark=?"); ps.add(remark); }
|
||||
if (sets.isEmpty()) return ResponseEntity.ok().build();
|
||||
String sql = "UPDATE vip_users SET " + String.join(",", sets) + ", updated_at=NOW() WHERE id=?";
|
||||
ps.add(id);
|
||||
jdbcTemplate.update(sql, ps.toArray());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/approve")
|
||||
public ResponseEntity<?> approve(@PathVariable("id") Long id,
|
||||
@RequestHeader(name = "X-User-Id") Long reviewerId) {
|
||||
jdbcTemplate.update("UPDATE vip_users SET status=1, reviewer_id=?, reviewed_at=NOW(), updated_at=NOW() WHERE id=?", reviewerId, id);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/reject")
|
||||
public ResponseEntity<?> reject(@PathVariable("id") Long id,
|
||||
@RequestHeader(name = "X-User-Id") Long reviewerId) {
|
||||
jdbcTemplate.update("UPDATE vip_users SET status=0, reviewer_id=?, reviewed_at=NOW(), updated_at=NOW() WHERE id=?", reviewerId, id);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
private static String str(Object v){ return v==null?null:String.valueOf(v); }
|
||||
private static Integer asInt(Object v){ try { return v==null?null:Integer.valueOf(String.valueOf(v)); } catch(Exception e){ return null; } }
|
||||
private static Integer asIntOr(Object v, int d){ Integer i = asInt(v); return i==null?d:i; }
|
||||
private static Long asLong(Object v){ try { return v==null?null:Long.valueOf(String.valueOf(v)); } catch(Exception e){ return null; } }
|
||||
private static java.sql.Timestamp asTimestamp(Object v){
|
||||
if (v == null) return null;
|
||||
try { return java.sql.Timestamp.valueOf(String.valueOf(v).replace('T',' ').replace('Z',' ')); } catch(Exception e){ return null; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.demo.auth;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "jwt")
|
||||
public class JwtProperties {
|
||||
private String secret;
|
||||
private String issuer = "parts-inquiry-api";
|
||||
private long ttlSeconds = 7200;
|
||||
private long clockSkewSeconds = 60;
|
||||
|
||||
public String getSecret() { return secret; }
|
||||
public void setSecret(String secret) { this.secret = secret; }
|
||||
public String getIssuer() { return issuer; }
|
||||
public void setIssuer(String issuer) { this.issuer = issuer; }
|
||||
public long getTtlSeconds() { return ttlSeconds; }
|
||||
public void setTtlSeconds(long ttlSeconds) { this.ttlSeconds = ttlSeconds; }
|
||||
public long getClockSkewSeconds() { return clockSkewSeconds; }
|
||||
public void setClockSkewSeconds(long clockSkewSeconds) { this.clockSkewSeconds = clockSkewSeconds; }
|
||||
}
|
||||
|
||||
|
||||
59
backend/src/main/java/com/example/demo/auth/JwtService.java
Normal file
59
backend/src/main/java/com/example/demo/auth/JwtService.java
Normal file
@@ -0,0 +1,59 @@
|
||||
package com.example.demo.auth;
|
||||
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.algorithms.Algorithm;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import com.auth0.jwt.interfaces.JWTVerifier;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class JwtService {
|
||||
private final JwtProperties props;
|
||||
|
||||
public JwtService(JwtProperties props) {
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
public String signToken(Long userId, Long shopId, String phone, String provider) {
|
||||
Instant now = Instant.now();
|
||||
Algorithm alg = Algorithm.HMAC256(props.getSecret() == null ? "dev-secret" : props.getSecret());
|
||||
var jwt = JWT.create()
|
||||
.withIssuer(props.getIssuer())
|
||||
.withIssuedAt(java.util.Date.from(now))
|
||||
.withExpiresAt(java.util.Date.from(now.plusSeconds(props.getTtlSeconds())))
|
||||
.withClaim("userId", userId)
|
||||
.withClaim("shopId", shopId)
|
||||
.withClaim("provider", provider);
|
||||
if (phone != null && !phone.isBlank()) jwt.withClaim("phone", phone);
|
||||
return jwt.sign(alg);
|
||||
}
|
||||
|
||||
public Map<String,Object> parseClaims(String authorizationHeader) {
|
||||
Map<String,Object> out = new HashMap<>();
|
||||
if (authorizationHeader == null || authorizationHeader.isBlank()) return out;
|
||||
String prefix = "Bearer ";
|
||||
if (!authorizationHeader.startsWith(prefix)) return out;
|
||||
String token = authorizationHeader.substring(prefix.length()).trim();
|
||||
try {
|
||||
Algorithm alg = Algorithm.HMAC256(props.getSecret() == null ? "dev-secret" : props.getSecret());
|
||||
JWTVerifier verifier = JWT.require(alg)
|
||||
.withIssuer(props.getIssuer())
|
||||
.acceptLeeway(props.getClockSkewSeconds())
|
||||
.build();
|
||||
DecodedJWT jwt = verifier.verify(token);
|
||||
Long userId = jwt.getClaim("userId").asLong();
|
||||
Long shopId = jwt.getClaim("shopId").asLong();
|
||||
String phone = jwt.getClaim("phone").asString();
|
||||
if (userId != null) out.put("userId", userId);
|
||||
if (shopId != null) out.put("shopId", shopId);
|
||||
if (phone != null && !phone.isBlank()) out.put("phone", phone);
|
||||
} catch (Exception ignore) { }
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.demo.auth;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
public class PasswordAuthController {
|
||||
|
||||
private final PasswordAuthService service;
|
||||
|
||||
public PasswordAuthController(PasswordAuthService service) { this.service = service; }
|
||||
|
||||
@PostMapping("/password/login")
|
||||
public ResponseEntity<?> login(@RequestBody PasswordAuthService.LoginRequest req) {
|
||||
var resp = service.login(req);
|
||||
return ResponseEntity.ok(resp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.example.demo.auth;
|
||||
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class PasswordAuthService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final JwtService jwtService;
|
||||
private final JwtProperties jwtProps;
|
||||
|
||||
public PasswordAuthService(JdbcTemplate jdbcTemplate,
|
||||
JwtService jwtService,
|
||||
JwtProperties jwtProps) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.jwtService = jwtService;
|
||||
this.jwtProps = jwtProps;
|
||||
}
|
||||
|
||||
public static class LoginRequest { public String phone; public String password; }
|
||||
public static class LoginResponse { public String token; public long expiresIn; public Map<String,Object> user; }
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public LoginResponse login(LoginRequest req) {
|
||||
ensurePhoneFormat(req.phone);
|
||||
if (req.password == null || req.password.isBlank()) throw new IllegalArgumentException("密码不能为空");
|
||||
|
||||
Map<String, Object> row = jdbcTemplate.query(
|
||||
con -> {
|
||||
var ps = con.prepareStatement("SELECT id, shop_id, password_hash, status FROM users WHERE phone=? LIMIT 1");
|
||||
ps.setString(1, req.phone);
|
||||
return ps;
|
||||
},
|
||||
rs -> {
|
||||
if (rs.next()) {
|
||||
Map<String,Object> m = new HashMap<>();
|
||||
m.put("id", rs.getLong(1));
|
||||
m.put("shop_id", rs.getLong(2));
|
||||
m.put("password_hash", rs.getString(3));
|
||||
m.put("status", rs.getInt(4));
|
||||
return m;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
if (row == null) throw new IllegalArgumentException("用户不存在");
|
||||
int status = ((Number)row.get("status")).intValue();
|
||||
if (status != 1) throw new IllegalArgumentException("用户未启用");
|
||||
String hash = (String) row.get("password_hash");
|
||||
if (hash == null || hash.isBlank()) throw new IllegalArgumentException("NO_PASSWORD");
|
||||
boolean ok = org.springframework.security.crypto.bcrypt.BCrypt.checkpw(req.password, hash);
|
||||
if (!ok) throw new IllegalArgumentException("密码错误");
|
||||
|
||||
Long userId = ((Number)row.get("id")).longValue();
|
||||
Long shopId = ((Number)row.get("shop_id")).longValue();
|
||||
|
||||
String token = jwtService.signToken(userId, shopId, req.phone, "password");
|
||||
LoginResponse out = new LoginResponse();
|
||||
out.token = token;
|
||||
out.expiresIn = jwtProps.getTtlSeconds();
|
||||
Map<String,Object> userMap = new HashMap<>();
|
||||
userMap.put("userId", userId); userMap.put("shopId", shopId); userMap.put("phone", req.phone);
|
||||
out.user = userMap;
|
||||
return out;
|
||||
}
|
||||
|
||||
private void ensurePhoneFormat(String phone) {
|
||||
if (phone == null || phone.isBlank()) throw new IllegalArgumentException("手机号不能为空");
|
||||
String p = phone.replaceAll("\\s+", "");
|
||||
if (!p.matches("^1\\d{10}$")) throw new IllegalArgumentException("手机号格式不正确");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.example.demo.auth;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
public class RegisterController {
|
||||
|
||||
private final RegisterService registerService;
|
||||
|
||||
public RegisterController(RegisterService registerService) {
|
||||
this.registerService = registerService;
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
public ResponseEntity<?> register(@RequestBody RegisterService.RegisterRequest req) {
|
||||
var resp = registerService.register(req);
|
||||
return ResponseEntity.ok(resp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
156
backend/src/main/java/com/example/demo/auth/RegisterService.java
Normal file
156
backend/src/main/java/com/example/demo/auth/RegisterService.java
Normal file
@@ -0,0 +1,156 @@
|
||||
package com.example.demo.auth;
|
||||
|
||||
import com.example.demo.common.AppDefaultsProperties;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import com.example.demo.common.ShopDefaultsProperties;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.support.GeneratedKeyHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class RegisterService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final JwtService jwtService;
|
||||
private final JwtProperties jwtProps;
|
||||
private final ShopDefaultsProperties shopDefaults;
|
||||
private AppDefaultsProperties appDefaults;
|
||||
|
||||
public RegisterService(JdbcTemplate jdbcTemplate,
|
||||
JwtService jwtService,
|
||||
JwtProperties jwtProps,
|
||||
ShopDefaultsProperties shopDefaults) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.jwtService = jwtService;
|
||||
this.jwtProps = jwtProps;
|
||||
this.shopDefaults = shopDefaults;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setAppDefaults(AppDefaultsProperties appDefaults) {
|
||||
this.appDefaults = appDefaults;
|
||||
}
|
||||
|
||||
private String hashPassword(String raw) {
|
||||
try {
|
||||
return org.springframework.security.crypto.bcrypt.BCrypt.hashpw(raw, org.springframework.security.crypto.bcrypt.BCrypt.gensalt(10));
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("密码加密失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class RegisterRequest {
|
||||
public String phone; // 必填:11位
|
||||
public String name; // 可选:默认用脱敏手机号
|
||||
public String password; // 可选:如提供则保存密码哈希
|
||||
}
|
||||
|
||||
public static class RegisterResponse {
|
||||
public String token;
|
||||
public long expiresIn;
|
||||
public Map<String,Object> user;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public RegisterResponse register(RegisterRequest req) {
|
||||
ensurePhoneFormat(req.phone);
|
||||
String phone = req.phone.trim();
|
||||
String displayName = (req.name == null || req.name.isBlank()) ? maskPhoneForName(phone) : req.name.trim();
|
||||
|
||||
// 已存在则直接签发令牌
|
||||
var existing = jdbcTemplate.queryForList("SELECT id, shop_id FROM users WHERE phone=? LIMIT 1", phone);
|
||||
Long userId;
|
||||
Long shopId;
|
||||
if (!existing.isEmpty()) {
|
||||
Map<String,Object> row = existing.get(0);
|
||||
userId = ((Number)row.get("id")).longValue();
|
||||
shopId = ((Number)row.get("shop_id")).longValue();
|
||||
} else {
|
||||
// 1) 创建店铺
|
||||
String shopName = String.format(shopDefaults.getNamePattern(), displayName);
|
||||
GeneratedKeyHolder shopKey = new GeneratedKeyHolder();
|
||||
jdbcTemplate.update(con -> {
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO shops(name, status, created_at, updated_at) VALUES (?,1,NOW(),NOW())",
|
||||
java.sql.Statement.RETURN_GENERATED_KEYS);
|
||||
ps.setString(1, shopName);
|
||||
return ps;
|
||||
}, shopKey);
|
||||
Number shopGenId = shopKey.getKey();
|
||||
if (shopGenId == null) throw new IllegalStateException("创建店铺失败");
|
||||
shopId = shopGenId.longValue();
|
||||
|
||||
// 2) 创建店主用户(owner)
|
||||
GeneratedKeyHolder userKey = new GeneratedKeyHolder();
|
||||
final Long sid = shopId;
|
||||
jdbcTemplate.update(con -> {
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO users(shop_id, phone, name, role, password_hash, status, is_owner, created_at, updated_at) " +
|
||||
"VALUES (?,?,?,?,?,1,1,NOW(),NOW())",
|
||||
java.sql.Statement.RETURN_GENERATED_KEYS);
|
||||
ps.setLong(1, sid);
|
||||
ps.setString(2, phone);
|
||||
ps.setString(3, displayName);
|
||||
ps.setString(4, shopDefaults.getOwnerRole());
|
||||
// 如提供密码,存入哈希;否则设为 NULL
|
||||
String pwd = (req.password == null || req.password.isBlank()) ? null : hashPassword(req.password);
|
||||
if (pwd != null) ps.setString(5, pwd); else ps.setNull(5, java.sql.Types.VARCHAR);
|
||||
return ps;
|
||||
}, userKey);
|
||||
Number userGenId = userKey.getKey();
|
||||
if (userGenId == null) throw new IllegalStateException("创建用户失败");
|
||||
userId = userGenId.longValue();
|
||||
|
||||
// 3) 创建默认账户(现金/银行存款/微信)
|
||||
createDefaultAccounts(shopId, userId);
|
||||
}
|
||||
|
||||
String token = jwtService.signToken(userId, shopId, phone, "register");
|
||||
RegisterResponse out = new RegisterResponse();
|
||||
out.token = token;
|
||||
out.expiresIn = jwtProps.getTtlSeconds();
|
||||
HashMap<String,Object> userMap = new HashMap<>();
|
||||
userMap.put("userId", userId);
|
||||
userMap.put("shopId", shopId);
|
||||
userMap.put("phone", phone);
|
||||
out.user = userMap;
|
||||
return out;
|
||||
}
|
||||
|
||||
private void createDefaultAccounts(Long shopId, Long userId) {
|
||||
// 现金
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO accounts(shop_id,user_id,name,`type`,balance,status,created_at,updated_at) " +
|
||||
"SELECT ?, ?, ?, 'cash', 0, 1, NOW(), NOW() FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM accounts WHERE shop_id=? AND name=?)",
|
||||
shopId, userId, appDefaults.getAccountCashName(), shopId, appDefaults.getAccountCashName());
|
||||
// 银行存款
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO accounts(shop_id,user_id,name,`type`,balance,status,created_at,updated_at) " +
|
||||
"SELECT ?, ?, ?, 'bank', 0, 1, NOW(), NOW() FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM accounts WHERE shop_id=? AND name=?)",
|
||||
shopId, userId, appDefaults.getAccountBankName(), shopId, appDefaults.getAccountBankName());
|
||||
// 微信
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO accounts(shop_id,user_id,name,`type`,balance,status,created_at,updated_at) " +
|
||||
"SELECT ?, ?, ?, 'wechat', 0, 1, NOW(), NOW() FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM accounts WHERE shop_id=? AND name=?)",
|
||||
shopId, userId, appDefaults.getAccountWechatName(), shopId, appDefaults.getAccountWechatName());
|
||||
}
|
||||
|
||||
private void ensurePhoneFormat(String phone) {
|
||||
if (phone == null || phone.isBlank()) throw new IllegalArgumentException("手机号不能为空");
|
||||
String p = phone.replaceAll("\\s+", "");
|
||||
if (!p.matches("^1\\d{10}$")) throw new IllegalArgumentException("手机号格式不正确");
|
||||
}
|
||||
|
||||
private static String maskPhoneForName(String phone) {
|
||||
String p = String.valueOf(phone);
|
||||
if (p.length() == 11) return "用户" + p.substring(0,3) + "****" + p.substring(7);
|
||||
return "手机用户";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.example.demo.auth;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/auth/sms")
|
||||
public class SmsAuthController {
|
||||
|
||||
private final SmsAuthService smsAuthService;
|
||||
|
||||
public SmsAuthController(SmsAuthService smsAuthService) {
|
||||
this.smsAuthService = smsAuthService;
|
||||
}
|
||||
|
||||
@PostMapping("/send")
|
||||
public ResponseEntity<?> send(@RequestBody SmsAuthService.SendCodeRequest req,
|
||||
@RequestHeader(value = "X-Forwarded-For", required = false) String xff,
|
||||
@RequestHeader(value = "X-Real-IP", required = false) String xri,
|
||||
@RequestHeader(value = "X-Shop-Id", required = false) Long shopId) {
|
||||
String ip = xri != null ? xri : (xff != null ? xff.split(",")[0].trim() : getClientIp());
|
||||
SmsAuthService.SendCodeResponse resp = smsAuthService.sendCode(req, ip);
|
||||
return ResponseEntity.ok(resp);
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<?> login(@RequestBody SmsAuthService.LoginRequest req) {
|
||||
SmsAuthService.LoginResponse resp = smsAuthService.login(req);
|
||||
return ResponseEntity.ok(resp);
|
||||
}
|
||||
|
||||
private String getClientIp() {
|
||||
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
|
||||
if (attrs instanceof ServletRequestAttributes sra) {
|
||||
var req = sra.getRequest();
|
||||
return req.getRemoteAddr();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
199
backend/src/main/java/com/example/demo/auth/SmsAuthService.java
Normal file
199
backend/src/main/java/com/example/demo/auth/SmsAuthService.java
Normal file
@@ -0,0 +1,199 @@
|
||||
package com.example.demo.auth;
|
||||
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class SmsAuthService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final JwtService jwtService;
|
||||
private final JwtProperties jwtProps;
|
||||
private final com.example.demo.common.ShopDefaultsProperties shopDefaults;
|
||||
|
||||
public SmsAuthService(JdbcTemplate jdbcTemplate,
|
||||
JwtService jwtService,
|
||||
JwtProperties jwtProps,
|
||||
com.example.demo.common.ShopDefaultsProperties shopDefaults) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.jwtService = jwtService;
|
||||
this.jwtProps = jwtProps;
|
||||
this.shopDefaults = shopDefaults;
|
||||
}
|
||||
|
||||
public static class SendCodeRequest { public String phone; public String scene; }
|
||||
public static class SendCodeResponse { public boolean ok; public long cooldownSec; }
|
||||
public static class LoginRequest { public String phone; public String code; }
|
||||
public static class LoginResponse { public String token; public long expiresIn; public Map<String,Object> user; }
|
||||
|
||||
private String generateCode() {
|
||||
SecureRandom rng = new SecureRandom();
|
||||
int n = 100000 + rng.nextInt(900000);
|
||||
return String.valueOf(n);
|
||||
}
|
||||
|
||||
private static String hmacSha256Hex(String secret, String message) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
md.update((message + secret).getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
return HexFormat.of().formatHex(md.digest());
|
||||
} catch (Exception e) { throw new RuntimeException(e); }
|
||||
}
|
||||
|
||||
private void ensurePhoneFormat(String phone) {
|
||||
if (phone == null || phone.isBlank()) throw new IllegalArgumentException("手机号不能为空");
|
||||
String p = phone.replaceAll("\\s+", "");
|
||||
if (!p.matches("^1\\d{10}$")) throw new IllegalArgumentException("手机号格式不正确");
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public SendCodeResponse sendCode(SendCodeRequest req, String clientIp) {
|
||||
ensurePhoneFormat(req.phone);
|
||||
String phone = req.phone;
|
||||
String scene = (req.scene == null || req.scene.isBlank()) ? "login" : req.scene;
|
||||
|
||||
Long cntRecent = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(1) FROM sms_codes WHERE phone=? AND scene=? AND created_at >= NOW() - INTERVAL 60 SECOND",
|
||||
Long.class, phone, scene);
|
||||
if (cntRecent != null && cntRecent > 0) {
|
||||
SendCodeResponse out = new SendCodeResponse();
|
||||
out.ok = false; out.cooldownSec = 60;
|
||||
return out;
|
||||
}
|
||||
|
||||
String code = generateCode();
|
||||
String salt = Long.toHexString(System.nanoTime());
|
||||
String codeHash = hmacSha256Hex(salt, code);
|
||||
int ttl = 300; // 五分钟有效
|
||||
jdbcTemplate.update(con -> {
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO sms_codes(phone, scene, code_hash, salt, expire_at, status, fail_count, ip, created_at, updated_at) " +
|
||||
"VALUES (?,?,?,?,DATE_ADD(NOW(), INTERVAL ? SECOND),0,0,?,NOW(),NOW())");
|
||||
ps.setString(1, phone);
|
||||
ps.setString(2, scene);
|
||||
ps.setString(3, codeHash);
|
||||
ps.setString(4, salt);
|
||||
ps.setInt(5, ttl);
|
||||
ps.setString(6, clientIp);
|
||||
return ps;
|
||||
});
|
||||
|
||||
// TODO: 集成真实短信发送;当前仅存表,不外发
|
||||
|
||||
SendCodeResponse out = new SendCodeResponse();
|
||||
out.ok = true; out.cooldownSec = 60;
|
||||
return out;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public LoginResponse login(LoginRequest req) {
|
||||
ensurePhoneFormat(req.phone);
|
||||
if (req.code == null || req.code.isBlank()) throw new IllegalArgumentException("验证码不能为空");
|
||||
String phone = req.phone;
|
||||
|
||||
Map<String,Object> row = jdbcTemplate.query(
|
||||
con -> {
|
||||
var ps = con.prepareStatement("SELECT id, code_hash, salt, expire_at, status, fail_count FROM sms_codes WHERE phone=? AND scene='login' ORDER BY id DESC LIMIT 1");
|
||||
ps.setString(1, phone);
|
||||
return ps;
|
||||
},
|
||||
rs -> {
|
||||
if (rs.next()) {
|
||||
java.util.HashMap<String,Object> m = new java.util.HashMap<>();
|
||||
m.put("id", rs.getLong(1));
|
||||
m.put("code_hash", rs.getString(2));
|
||||
m.put("salt", rs.getString(3));
|
||||
m.put("expire_at", rs.getTimestamp(4));
|
||||
m.put("status", rs.getInt(5));
|
||||
m.put("fail_count", rs.getInt(6));
|
||||
return m;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
if (row == null) throw new IllegalArgumentException("CODE_INVALID");
|
||||
int status = (Integer) row.get("status");
|
||||
if (status != 0) throw new IllegalArgumentException("CODE_INVALID");
|
||||
java.sql.Timestamp expireAt = (java.sql.Timestamp) row.get("expire_at");
|
||||
if (expireAt.before(new java.util.Date())) {
|
||||
jdbcTemplate.update("UPDATE sms_codes SET status=2 WHERE id=?", (Long) row.get("id"));
|
||||
throw new IllegalArgumentException("CODE_EXPIRED");
|
||||
}
|
||||
int failCount = (Integer) row.get("fail_count");
|
||||
if (failCount >= 5) throw new IllegalArgumentException("TOO_MANY_FAILS");
|
||||
|
||||
String expect = (String) row.get("code_hash");
|
||||
String salt = (String) row.get("salt");
|
||||
String actual = hmacSha256Hex(salt, req.code);
|
||||
if (!actual.equalsIgnoreCase(expect)) {
|
||||
jdbcTemplate.update("UPDATE sms_codes SET fail_count=fail_count+1 WHERE id=?", (Long) row.get("id"));
|
||||
throw new IllegalArgumentException("CODE_INVALID");
|
||||
}
|
||||
|
||||
jdbcTemplate.update("UPDATE sms_codes SET status=1 WHERE id=?", (Long) row.get("id"));
|
||||
|
||||
List<Long> existing = jdbcTemplate.queryForList("SELECT id FROM users WHERE phone=? LIMIT 1", Long.class, phone);
|
||||
Long userId;
|
||||
Long shopId;
|
||||
if (!existing.isEmpty()) {
|
||||
userId = existing.get(0);
|
||||
List<Long> sids = jdbcTemplate.queryForList("SELECT shop_id FROM users WHERE id=?", Long.class, userId);
|
||||
shopId = sids.isEmpty() ? 1L : sids.get(0);
|
||||
} else {
|
||||
String userName = maskPhoneForName(phone);
|
||||
String shopName = String.format(shopDefaults.getNamePattern(), userName);
|
||||
var shopKey = new org.springframework.jdbc.support.GeneratedKeyHolder();
|
||||
jdbcTemplate.update(con -> {
|
||||
var ps = con.prepareStatement("INSERT INTO shops(name, status, created_at, updated_at) VALUES (?,1,NOW(),NOW())", java.sql.Statement.RETURN_GENERATED_KEYS);
|
||||
ps.setString(1, shopName);
|
||||
return ps;
|
||||
}, shopKey);
|
||||
Number shopGenId = shopKey.getKey();
|
||||
if (shopGenId == null) throw new IllegalStateException("创建店铺失败");
|
||||
shopId = shopGenId.longValue();
|
||||
|
||||
var userKey = new org.springframework.jdbc.support.GeneratedKeyHolder();
|
||||
final Long sid = shopId;
|
||||
jdbcTemplate.update(con -> {
|
||||
var ps = con.prepareStatement("INSERT INTO users(shop_id, phone, name, role, status, is_owner, created_at, updated_at) VALUES (?,?,?,?,1,1,NOW(),NOW())", java.sql.Statement.RETURN_GENERATED_KEYS);
|
||||
ps.setLong(1, sid);
|
||||
ps.setString(2, phone);
|
||||
ps.setString(3, userName);
|
||||
ps.setString(4, shopDefaults.getOwnerRole());
|
||||
return ps;
|
||||
}, userKey);
|
||||
Number userGenId = userKey.getKey();
|
||||
if (userGenId == null) throw new IllegalStateException("创建用户失败");
|
||||
userId = userGenId.longValue();
|
||||
}
|
||||
|
||||
String token = jwtService.signToken(userId, shopId, phone, "sms_otp");
|
||||
LoginResponse out = new LoginResponse();
|
||||
out.token = token;
|
||||
out.expiresIn = jwtProps.getTtlSeconds();
|
||||
java.util.HashMap<String,Object> userMap = new java.util.HashMap<>();
|
||||
userMap.put("userId", userId);
|
||||
userMap.put("shopId", shopId);
|
||||
userMap.put("phone", phone);
|
||||
out.user = userMap;
|
||||
return out;
|
||||
}
|
||||
|
||||
private static String maskPhoneForName(String phone) {
|
||||
String p = String.valueOf(phone);
|
||||
if (p.length() == 11) {
|
||||
return "用户" + p.substring(0,3) + "****" + p.substring(7);
|
||||
}
|
||||
return "手机用户";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.example.demo.common;
|
||||
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.lang.NonNull;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
@Component
|
||||
public class AdminAuthInterceptor implements HandlerInterceptor {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public AdminAuthInterceptor(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception {
|
||||
// 预检请求直接放行(由 CORS 处理器返回允许头)
|
||||
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
|
||||
return true;
|
||||
}
|
||||
// 简化版:临时从 X-User-Id 读取管理员身份,后续可改为 JWT
|
||||
String userIdHeader = request.getHeader("X-User-Id");
|
||||
if (userIdHeader == null || userIdHeader.isBlank()) {
|
||||
response.sendError(401, "missing X-User-Id");
|
||||
return false;
|
||||
}
|
||||
Long uid;
|
||||
try { uid = Long.valueOf(userIdHeader); } catch (Exception e) { response.sendError(401, "invalid user"); return false; }
|
||||
Integer admin = jdbcTemplate.queryForObject("SELECT is_platform_admin FROM users WHERE id=? LIMIT 1", Integer.class, uid);
|
||||
if (admin == null || admin != 1) {
|
||||
response.sendError(403, "forbidden");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ public class AppDefaultsProperties {
|
||||
|
||||
private Long shopId = 1L;
|
||||
private Long userId = 2L;
|
||||
// 字典(全局)使用的虚拟店铺ID,方案A:shop_id=0 代表全局共享
|
||||
private Long dictShopId = 0L;
|
||||
|
||||
// 默认账户名称(可配置,避免硬编码)
|
||||
private String accountCashName = "现金";
|
||||
@@ -21,6 +23,9 @@ public class AppDefaultsProperties {
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
|
||||
public Long getDictShopId() { return dictShopId; }
|
||||
public void setDictShopId(Long dictShopId) { this.dictShopId = dictShopId; }
|
||||
|
||||
public String getAccountCashName() { return accountCashName; }
|
||||
public void setAccountCashName(String accountCashName) { this.accountCashName = accountCashName; }
|
||||
public String getAccountBankName() { return accountBankName; }
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.example.demo.common;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@Configuration
|
||||
public class CorsConfig implements WebMvcConfigurer {
|
||||
|
||||
@Value("${app.cors.allowed-origins:*}")
|
||||
private String allowedOrigins;
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
String[] origins = Arrays.stream(allowedOrigins.split(","))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.toArray(String[]::new);
|
||||
registry.addMapping("/**")
|
||||
.allowedOrigins(origins)
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.exposedHeaders("Content-Disposition")
|
||||
.allowCredentials(false)
|
||||
.maxAge(3600);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -91,14 +91,17 @@ public class FinanceService {
|
||||
|
||||
private List<Map<String, String>> queryCategoriesFromTable(Long shopId, String type) {
|
||||
Long fallbackShopId = appDefaults == null ? 1L : (appDefaults.getShopId() == null ? 1L : appDefaults.getShopId());
|
||||
Long dictShopId = appDefaults == null ? 1000L : (appDefaults.getDictShopId() == null ? 1000L : appDefaults.getDictShopId());
|
||||
try (java.sql.Connection c = dataSource.getConnection();
|
||||
java.sql.PreparedStatement ps = c.prepareStatement(
|
||||
"SELECT shop_id, `key`, label FROM finance_categories WHERE shop_id IN (?, ?) AND type=? AND status=1 " +
|
||||
"ORDER BY CASE WHEN shop_id=? THEN 0 ELSE 1 END, sort_order, id")) {
|
||||
"SELECT shop_id, `key`, label FROM finance_categories WHERE shop_id IN (?, ?, ?) AND type=? AND status=1 " +
|
||||
"ORDER BY CASE WHEN shop_id=? THEN 0 WHEN shop_id=? THEN 1 ELSE 2 END, sort_order, id")) {
|
||||
ps.setLong(1, shopId);
|
||||
ps.setLong(2, fallbackShopId);
|
||||
ps.setString(3, type);
|
||||
ps.setLong(4, shopId);
|
||||
ps.setLong(3, dictShopId);
|
||||
ps.setString(4, type);
|
||||
ps.setLong(5, shopId);
|
||||
ps.setLong(6, dictShopId);
|
||||
try (java.sql.ResultSet rs = ps.executeQuery()) {
|
||||
java.util.Map<String,String> firstByKey = new java.util.LinkedHashMap<>();
|
||||
while (rs.next()) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.example.demo.common;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
@@ -11,6 +13,8 @@ import java.util.Map;
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||
|
||||
private ResponseEntity<Map<String,Object>> badRequest(String message) {
|
||||
Map<String,Object> body = new HashMap<>();
|
||||
body.put("message", message);
|
||||
@@ -19,23 +23,29 @@ public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public ResponseEntity<?> handleIllegalArgument(IllegalArgumentException ex) {
|
||||
log.warn("Bad request: {}", ex.getMessage());
|
||||
return badRequest(ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalStateException.class)
|
||||
public ResponseEntity<?> handleIllegalState(IllegalStateException ex) {
|
||||
log.warn("Illegal state: {}", ex.getMessage());
|
||||
return badRequest(ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(DataAccessException.class)
|
||||
public ResponseEntity<?> handleDataAccess(DataAccessException ex) {
|
||||
log.error("DataAccessException", ex);
|
||||
return badRequest("数据库操作失败");
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<?> handleAny(Exception ex) {
|
||||
log.error("Unhandled exception", ex);
|
||||
Map<String,Object> body = new HashMap<>();
|
||||
body.put("message", ex.getMessage() == null ? "Internal Server Error" : ex.getMessage());
|
||||
// 附加异常类型,便于前端调试
|
||||
body.put("error", ex.getClass().getSimpleName());
|
||||
return ResponseEntity.status(500).body(body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,19 @@ public class RequestLoggingFilter extends OncePerRequestFilter {
|
||||
String query = request.getQueryString();
|
||||
String shopId = request.getHeader("X-Shop-Id");
|
||||
String userId = request.getHeader("X-User-Id");
|
||||
if ((shopId == null || shopId.isBlank()) && (userId == null || userId.isBlank())) {
|
||||
String auth = request.getHeader("Authorization");
|
||||
try {
|
||||
com.example.demo.auth.JwtService jwtSvc = org.springframework.web.context.support.WebApplicationContextUtils
|
||||
.getRequiredWebApplicationContext(request.getServletContext())
|
||||
.getBean(com.example.demo.auth.JwtService.class);
|
||||
java.util.Map<String,Object> claims = jwtSvc.parseClaims(auth);
|
||||
Object sid = claims.get("shopId");
|
||||
Object uid = claims.get("userId");
|
||||
if (sid != null) shopId = String.valueOf(sid);
|
||||
if (uid != null) userId = String.valueOf(uid);
|
||||
} catch (Exception ignore) { }
|
||||
}
|
||||
|
||||
try {
|
||||
filterChain.doFilter(request, response);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.example.demo.common;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "app.shop")
|
||||
public class ShopDefaultsProperties {
|
||||
|
||||
// 店铺名称规则,使用 String.format 占位:%s -> 用户名
|
||||
private String namePattern = "%s_1";
|
||||
|
||||
// 店主角色名称
|
||||
private String ownerRole = "owner";
|
||||
|
||||
public String getNamePattern() { return namePattern; }
|
||||
public void setNamePattern(String namePattern) { this.namePattern = namePattern; }
|
||||
|
||||
public String getOwnerRole() { return ownerRole; }
|
||||
public void setOwnerRole(String ownerRole) { this.ownerRole = ownerRole; }
|
||||
}
|
||||
|
||||
|
||||
18
backend/src/main/java/com/example/demo/common/WebConfig.java
Normal file
18
backend/src/main/java/com/example/demo/common/WebConfig.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package com.example.demo.common;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
public WebConfig() { }
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 登录功能已移除,此处不再注册管理端鉴权拦截器
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,17 +26,15 @@ public class MetadataController {
|
||||
|
||||
@GetMapping("/api/product-units")
|
||||
public ResponseEntity<?> listUnits(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId) {
|
||||
Long sid = (shopId == null ? defaults.getShopId() : shopId);
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("list", unitRepository.listByShop(sid));
|
||||
body.put("list", unitRepository.listByShop(defaults.getDictShopId()));
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
@GetMapping("/api/product-categories")
|
||||
public ResponseEntity<?> listCategories(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId) {
|
||||
Long sid = (shopId == null ? defaults.getShopId() : shopId);
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("list", categoryRepository.listByShop(sid));
|
||||
body.put("list", categoryRepository.listByShop(defaults.getDictShopId()));
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import java.util.List;
|
||||
public interface CategoryRepository extends JpaRepository<ProductCategory, Long> {
|
||||
@Query("SELECT c FROM ProductCategory c WHERE c.shopId = :shopId AND c.deletedAt IS NULL ORDER BY c.sortOrder ASC, c.id DESC")
|
||||
List<ProductCategory> listByShop(@Param("shopId") Long shopId);
|
||||
|
||||
boolean existsByShopIdAndName(Long shopId, String name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import java.util.List;
|
||||
public interface UnitRepository extends JpaRepository<com.example.demo.product.entity.ProductUnit, Long> {
|
||||
@Query("SELECT u FROM ProductUnit u WHERE u.shopId = :shopId AND u.deletedAt IS NULL ORDER BY u.id DESC")
|
||||
List<com.example.demo.product.entity.ProductUnit> listByShop(@Param("shopId") Long shopId);
|
||||
|
||||
boolean existsByShopIdAndName(Long shopId, String name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -55,6 +55,8 @@ attachments.url.allowed-content-types=${ATTACHMENTS_URL_ALLOWED_CONTENT_TYPES:im
|
||||
# 应用默认上下文(用于开发/演示环境)
|
||||
app.defaults.shop-id=${APP_DEFAULT_SHOP_ID:1}
|
||||
app.defaults.user-id=${APP_DEFAULT_USER_ID:2}
|
||||
# 全局字典使用的虚拟店铺ID(方案A)
|
||||
app.defaults.dict-shop-id=${APP_DEFAULT_DICT_SHOP_ID:0}
|
||||
|
||||
# 财务分类默认配置(前端请调用 /api/finance/categories 获取,禁止硬编码)
|
||||
app.finance.income-categories=${APP_FINANCE_INCOME:operation_income:经营所得,interest_income:利息收入,other_income:其它收入,deposit_ar_income:收订金/欠款,investment_income:投资收入,sale_income:销售收入,account_operation:账户操作,fund_transfer_in:资金转账转入}
|
||||
@@ -65,3 +67,5 @@ app.account.defaults.cash-name=${APP_ACCOUNT_CASH_NAME:现金}
|
||||
app.account.defaults.bank-name=${APP_ACCOUNT_BANK_NAME:银行存款}
|
||||
app.account.defaults.wechat-name=${APP_ACCOUNT_WECHAT_NAME:微信}
|
||||
app.account.defaults.alipay-name=${APP_ACCOUNT_ALIPAY_NAME:支付宝}
|
||||
|
||||
# 登录相关配置已移除
|
||||
|
||||
Reference in New Issue
Block a user