From 39679f7330f1f016c58e0d47ddfa3f0d78bcef0b Mon Sep 17 00:00:00 2001 From: Wdp-ab <2182606194@qq.com> Date: Sun, 21 Sep 2025 16:01:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=BE=E7=89=87=E5=8A=9F=E8=83=BDurl?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../demo/attachment/AttachmentController.java | 38 ++- .../AttachmentUrlValidationProperties.java | 58 ++++ .../attachment/AttachmentUrlValidator.java | 250 ++++++++++++++++++ .../demo/dashboard/DashboardRepository.java | 3 +- .../demo/product/entity/ProductPrice.java | 5 - .../demo/product/repo/ProductRepository.java | 2 + .../demo/product/service/ProductService.java | 6 + .../src/main/resources/application.properties | 13 + doc/database_documentation.md | 109 +++++++- doc/openapi.yaml | 34 ++- frontend/main.js | 20 ++ .../dist/dev/.sourcemap/mp-weixin/app.js.map | 2 +- .../mp-weixin/components/ImageUploader.js.map | 2 +- frontend/unpackage/dist/dev/mp-weixin/app.js | 16 ++ 沟通.md | 53 +--- 15 files changed, 538 insertions(+), 73 deletions(-) create mode 100644 backend/src/main/java/com/example/demo/attachment/AttachmentUrlValidationProperties.java create mode 100644 backend/src/main/java/com/example/demo/attachment/AttachmentUrlValidator.java diff --git a/backend/src/main/java/com/example/demo/attachment/AttachmentController.java b/backend/src/main/java/com/example/demo/attachment/AttachmentController.java index 351bdc7..70718a8 100644 --- a/backend/src/main/java/com/example/demo/attachment/AttachmentController.java +++ b/backend/src/main/java/com/example/demo/attachment/AttachmentController.java @@ -9,6 +9,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -26,10 +27,13 @@ import java.util.concurrent.TimeUnit; public class AttachmentController { private final AttachmentPlaceholderProperties placeholderProperties; + private final AttachmentUrlValidator urlValidator; - public AttachmentController(AttachmentPlaceholderProperties placeholderProperties) { - this.placeholderProperties = placeholderProperties; - } + public AttachmentController(AttachmentPlaceholderProperties placeholderProperties, + AttachmentUrlValidator urlValidator) { + this.placeholderProperties = placeholderProperties; + this.urlValidator = urlValidator; + } @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity> upload(@RequestParam("file") MultipartFile file, @@ -42,6 +46,27 @@ public class AttachmentController { return ResponseEntity.ok(body); } + @PostMapping(path = "/validate-url", consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> validateUrlJson(@RequestBody Map body) { + String url = body == null ? null : String.valueOf(body.get("url")); + AttachmentUrlValidator.ValidationResult vr = urlValidator.validate(url); + Map resp = new HashMap<>(); + resp.put("url", vr.url()); + resp.put("contentType", vr.contentType()); + if (vr.contentLength() != null) resp.put("contentLength", vr.contentLength()); + return ResponseEntity.ok(resp); + } + + @PostMapping(path = "/validate-url", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + public ResponseEntity> validateUrlForm(@RequestParam("url") String url) { + AttachmentUrlValidator.ValidationResult vr = urlValidator.validate(url); + Map resp = new HashMap<>(); + resp.put("url", vr.url()); + resp.put("contentType", vr.contentType()); + if (vr.contentLength() != null) resp.put("contentLength", vr.contentLength()); + return ResponseEntity.ok(resp); + } + @GetMapping("/placeholder") public ResponseEntity placeholder() throws IOException { String imagePath = placeholderProperties.getImagePath(); @@ -53,7 +78,12 @@ public class AttachmentController { return ResponseEntity.status(404).build(); } Resource resource = new FileSystemResource(path); - String contentType = Files.probeContentType(path); + String contentType = null; + try { + contentType = Files.probeContentType(path); + } catch (IOException ignore) { + contentType = null; + } MediaType mediaType; try { mediaType = StringUtils.hasText(contentType) ? MediaType.parseMediaType(contentType) : MediaType.IMAGE_PNG; diff --git a/backend/src/main/java/com/example/demo/attachment/AttachmentUrlValidationProperties.java b/backend/src/main/java/com/example/demo/attachment/AttachmentUrlValidationProperties.java new file mode 100644 index 0000000..7fd4233 --- /dev/null +++ b/backend/src/main/java/com/example/demo/attachment/AttachmentUrlValidationProperties.java @@ -0,0 +1,58 @@ +package com.example.demo.attachment; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Component +@ConfigurationProperties(prefix = "attachments.url") +public class AttachmentUrlValidationProperties { + + private boolean ssrfProtection = true; + private boolean allowPrivateIp = false; + private boolean followRedirects = true; + private int maxRedirects = 2; + private int connectTimeoutMs = 3000; + private int readTimeoutMs = 5000; + private int maxSizeMb = 5; + private List allowlist = new ArrayList<>(); + private List allowedContentTypes = new ArrayList<>(Arrays.asList( + "image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml" + )); + + public boolean isSsrfProtection() { return ssrfProtection; } + public void setSsrfProtection(boolean ssrfProtection) { this.ssrfProtection = ssrfProtection; } + + public boolean isAllowPrivateIp() { return allowPrivateIp; } + public void setAllowPrivateIp(boolean allowPrivateIp) { this.allowPrivateIp = allowPrivateIp; } + + public boolean isFollowRedirects() { return followRedirects; } + public void setFollowRedirects(boolean followRedirects) { this.followRedirects = followRedirects; } + + public int getMaxRedirects() { return maxRedirects; } + public void setMaxRedirects(int maxRedirects) { this.maxRedirects = maxRedirects; } + + public int getConnectTimeoutMs() { return connectTimeoutMs; } + public void setConnectTimeoutMs(int connectTimeoutMs) { this.connectTimeoutMs = connectTimeoutMs; } + + public int getReadTimeoutMs() { return readTimeoutMs; } + public void setReadTimeoutMs(int readTimeoutMs) { this.readTimeoutMs = readTimeoutMs; } + + public int getMaxSizeMb() { return maxSizeMb; } + public void setMaxSizeMb(int maxSizeMb) { this.maxSizeMb = maxSizeMb; } + + public List getAllowlist() { return allowlist; } + public void setAllowlist(List allowlist) { this.allowlist = allowlist; } + + public List getAllowedContentTypes() { return allowedContentTypes; } + public void setAllowedContentTypes(List allowedContentTypes) { this.allowedContentTypes = allowedContentTypes; } + + public long getMaxSizeBytes() { + return (long) maxSizeMb * 1024L * 1024L; + } +} + + diff --git a/backend/src/main/java/com/example/demo/attachment/AttachmentUrlValidator.java b/backend/src/main/java/com/example/demo/attachment/AttachmentUrlValidator.java new file mode 100644 index 0000000..20d345c --- /dev/null +++ b/backend/src/main/java/com/example/demo/attachment/AttachmentUrlValidator.java @@ -0,0 +1,250 @@ +package com.example.demo.attachment; + +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.io.IOException; +import java.net.*; +import java.util.List; +import java.util.Locale; + +@Service +public class AttachmentUrlValidator { + + private final AttachmentUrlValidationProperties props; + + public AttachmentUrlValidator(AttachmentUrlValidationProperties props) { + this.props = props; + } + + public ValidationResult validate(String urlString) { + if (!StringUtils.hasText(urlString)) { + throw new IllegalArgumentException("url不能为空"); + } + + try { + URI uri = new URI(urlString.trim()); + if (!"http".equalsIgnoreCase(uri.getScheme()) && !"https".equalsIgnoreCase(uri.getScheme())) { + throw new IllegalArgumentException("仅支持http/https"); + } + if (!StringUtils.hasText(uri.getHost())) { + throw new IllegalArgumentException("URL缺少主机名"); + } + + // allowlist 校验 + enforceAllowlist(uri.getHost()); + + // SSRF/IP 私网校验 + if (props.isSsrfProtection()) { + enforceIpSafety(uri.getHost()); + } + + // 发起 HEAD 请求(必要时回退 GET Range)并处理重定向 + HttpResult http = headCheckWithRedirects(uri, props.getMaxRedirects()); + + // 内容类型校验 + String contentType = normalizeContentType(http.contentType); + if (!isAllowedContentType(contentType)) { + // 允许根据扩展名兜底一次 + String guessed = guessContentTypeFromPath(http.finalUri.getPath()); + if (!isAllowedContentType(guessed)) { + throw new IllegalArgumentException("不支持的图片类型"); + } + contentType = guessed; + } + + // 大小校验(若已知) + if (http.contentLength != null && http.contentLength > 0) { + if (http.contentLength > props.getMaxSizeBytes()) { + throw new IllegalArgumentException("图片过大,超过上限" + props.getMaxSizeMb() + "MB"); + } + } + + return new ValidationResult(http.finalUri.toString(), contentType, http.contentLength); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("URL不合法"); + } + } + + private void enforceAllowlist(String host) { + List list = props.getAllowlist(); + if (list == null || list.isEmpty()) return; + String h = host.toLowerCase(Locale.ROOT); + for (String item : list) { + if (!StringUtils.hasText(item)) continue; + String rule = item.trim().toLowerCase(Locale.ROOT); + if (rule.startsWith("*.")) { + String suf = rule.substring(1); // .example.com + if (h.endsWith(suf)) return; + } else { + if (h.equals(rule) || h.endsWith("." + rule)) return; + } + } + throw new IllegalArgumentException("域名不在白名单"); + } + + private void enforceIpSafety(String host) { + try { + InetAddress[] all = InetAddress.getAllByName(host); + for (InetAddress addr : all) { + if (!isPublicAddress(addr)) { + if (!props.isAllowPrivateIp()) { + throw new IllegalArgumentException("禁止访问内网/本地地址"); + } + } + } + } catch (UnknownHostException e) { + throw new IllegalArgumentException("无法解析域名"); + } + } + + private boolean isPublicAddress(InetAddress addr) { + return !(addr.isAnyLocalAddress() || addr.isLoopbackAddress() || addr.isLinkLocalAddress() || + addr.isSiteLocalAddress() || addr.isMulticastAddress()); + } + + private HttpResult headCheckWithRedirects(URI uri, int remainingRedirects) { + try { + URI current = uri; + int redirects = 0; + while (true) { + HttpURLConnection conn = (HttpURLConnection) current.toURL().openConnection(); + conn.setConnectTimeout(props.getConnectTimeoutMs()); + conn.setReadTimeout(props.getReadTimeoutMs()); + conn.setInstanceFollowRedirects(false); + conn.setRequestProperty("User-Agent", "PartsInquiry/1.0"); + try { + conn.setRequestMethod("HEAD"); + } catch (ProtocolException ignored) { + // 一些实现不允许设置,忽略 + } + + int code; + try { + code = conn.getResponseCode(); + } catch (IOException ex) { + // 回退到GET Range + return getRange0(current, redirects); + } + + if (isRedirect(code)) { + if (!props.isFollowRedirects() || redirects >= props.getMaxRedirects()) { + throw new IllegalArgumentException("重定向过多"); + } + String loc = conn.getHeaderField("Location"); + if (!StringUtils.hasText(loc)) { + throw new IllegalArgumentException("重定向无Location"); + } + URI next = current.resolve(loc); + if (!"http".equalsIgnoreCase(next.getScheme()) && !"https".equalsIgnoreCase(next.getScheme())) { + throw new IllegalArgumentException("非法重定向协议"); + } + // 重定向目标再做一次安全检查 + enforceAllowlist(next.getHost()); + if (props.isSsrfProtection()) enforceIpSafety(next.getHost()); + current = next; + redirects++; + continue; + } + + if (code >= 200 && code < 300) { + String ct = conn.getHeaderField("Content-Type"); + String cl = conn.getHeaderField("Content-Length"); + Long len = parseLongSafe(totalFromContentRange(conn.getHeaderField("Content-Range"), cl)); + return new HttpResult(current, ct, len); + } + + // HEAD 不被允许时(405等)回退到 GET Range + if (code == HttpURLConnection.HTTP_BAD_METHOD || code == HttpURLConnection.HTTP_FORBIDDEN) { + return getRange0(current, redirects); + } + + throw new IllegalArgumentException("URL不可访问,HTTP " + code); + } + } catch (IOException e) { + throw new IllegalArgumentException("无法访问URL"); + } + } + + private HttpResult getRange0(URI uri, int redirects) throws IOException { + HttpURLConnection conn = (HttpURLConnection) uri.toURL().openConnection(); + conn.setConnectTimeout(props.getConnectTimeoutMs()); + conn.setReadTimeout(props.getReadTimeoutMs()); + conn.setInstanceFollowRedirects(false); + conn.setRequestProperty("User-Agent", "PartsInquiry/1.0"); + conn.setRequestProperty("Range", "bytes=0-0"); + int code = conn.getResponseCode(); + if (isRedirect(code)) { + if (!props.isFollowRedirects() || redirects >= props.getMaxRedirects()) { + throw new IllegalArgumentException("重定向过多"); + } + String loc = conn.getHeaderField("Location"); + if (!StringUtils.hasText(loc)) throw new IllegalArgumentException("重定向无Location"); + URI next = uri.resolve(loc); + enforceAllowlist(next.getHost()); + if (props.isSsrfProtection()) enforceIpSafety(next.getHost()); + return headCheckWithRedirects(next, props.getMaxRedirects() - redirects - 1); + } + if (code >= 200 && code < 300 || code == HttpURLConnection.HTTP_PARTIAL) { + String ct = conn.getHeaderField("Content-Type"); + String cl = conn.getHeaderField("Content-Length"); + Long len = parseLongSafe(totalFromContentRange(conn.getHeaderField("Content-Range"), cl)); + return new HttpResult(uri, ct, len); + } + throw new IllegalArgumentException("URL不可访问,HTTP " + code); + } + + private boolean isRedirect(int code) { + return code == HttpURLConnection.HTTP_MOVED_PERM || code == HttpURLConnection.HTTP_MOVED_TEMP + || code == HttpURLConnection.HTTP_SEE_OTHER || code == 307 || code == 308; + } + + private String normalizeContentType(String ct) { + if (!StringUtils.hasText(ct)) return null; + int idx = ct.indexOf(';'); + String base = (idx > 0 ? ct.substring(0, idx) : ct).trim().toLowerCase(Locale.ROOT); + return base; + } + + private boolean isAllowedContentType(String ct) { + if (!StringUtils.hasText(ct)) return false; + for (String allowed : props.getAllowedContentTypes()) { + if (ct.equalsIgnoreCase(allowed)) return true; + } + return false; + } + + private String guessContentTypeFromPath(String path) { + if (path == null) return null; + String p = path.toLowerCase(Locale.ROOT); + if (p.endsWith(".jpg") || p.endsWith(".jpeg")) return "image/jpeg"; + if (p.endsWith(".png")) return "image/png"; + if (p.endsWith(".gif")) return "image/gif"; + if (p.endsWith(".webp")) return "image/webp"; + if (p.endsWith(".svg")) return "image/svg+xml"; + return null; + } + + private String totalFromContentRange(String contentRange, String fallbackLength) { + // Content-Range: bytes 0-0/12345 -> 12345 + if (contentRange != null) { + int slash = contentRange.lastIndexOf('/'); + if (slash > 0 && slash + 1 < contentRange.length()) { + String total = contentRange.substring(slash + 1).trim(); + if (!"*".equals(total)) return total; + } + } + return fallbackLength; + } + + private Long parseLongSafe(String v) { + if (!StringUtils.hasText(v)) return null; + try { return Long.parseLong(v.trim()); } catch (Exception e) { return null; } + } + + public record ValidationResult(String url, String contentType, Long contentLength) {} + + private record HttpResult(URI finalUri, String contentType, Long contentLength) {} +} + + diff --git a/backend/src/main/java/com/example/demo/dashboard/DashboardRepository.java b/backend/src/main/java/com/example/demo/dashboard/DashboardRepository.java index 6c6862c..50e58c4 100644 --- a/backend/src/main/java/com/example/demo/dashboard/DashboardRepository.java +++ b/backend/src/main/java/com/example/demo/dashboard/DashboardRepository.java @@ -15,7 +15,8 @@ public class DashboardRepository { public BigDecimal sumTodaySalesOrders(Long shopId) { Object result = entityManager.createNativeQuery( "SELECT COALESCE(SUM(amount), 0) FROM sales_orders " + - "WHERE shop_id = :shopId AND status = 'approved' AND DATE(order_time) = CURRENT_DATE()" + "WHERE shop_id = :shopId AND status = 'approved' " + + "AND order_time >= CURRENT_DATE() AND order_time < DATE_ADD(CURRENT_DATE(), INTERVAL 1 DAY)" ).setParameter("shopId", shopId).getSingleResult(); return toBigDecimal(result); } diff --git a/backend/src/main/java/com/example/demo/product/entity/ProductPrice.java b/backend/src/main/java/com/example/demo/product/entity/ProductPrice.java index b45eda9..ef07df3 100644 --- a/backend/src/main/java/com/example/demo/product/entity/ProductPrice.java +++ b/backend/src/main/java/com/example/demo/product/entity/ProductPrice.java @@ -24,9 +24,6 @@ public class ProductPrice { @Column(name = "retail_price", precision = 18, scale = 2, nullable = false) private BigDecimal retailPrice = BigDecimal.ZERO; - @Column(name = "distribution_price", precision = 18, scale = 2, nullable = false) - private BigDecimal distributionPrice = BigDecimal.ZERO; - @Column(name = "wholesale_price", precision = 18, scale = 2, nullable = false) private BigDecimal wholesalePrice = BigDecimal.ZERO; @@ -46,8 +43,6 @@ public class ProductPrice { public void setPurchasePrice(BigDecimal purchasePrice) { this.purchasePrice = purchasePrice; } public BigDecimal getRetailPrice() { return retailPrice; } public void setRetailPrice(BigDecimal retailPrice) { this.retailPrice = retailPrice; } - public BigDecimal getDistributionPrice() { return distributionPrice; } - public void setDistributionPrice(BigDecimal distributionPrice) { this.distributionPrice = distributionPrice; } public BigDecimal getWholesalePrice() { return wholesalePrice; } public void setWholesalePrice(BigDecimal wholesalePrice) { this.wholesalePrice = wholesalePrice; } public BigDecimal getBigClientPrice() { return bigClientPrice; } diff --git a/backend/src/main/java/com/example/demo/product/repo/ProductRepository.java b/backend/src/main/java/com/example/demo/product/repo/ProductRepository.java index 83f338f..935e54f 100644 --- a/backend/src/main/java/com/example/demo/product/repo/ProductRepository.java +++ b/backend/src/main/java/com/example/demo/product/repo/ProductRepository.java @@ -18,6 +18,8 @@ public interface ProductRepository extends JpaRepository { Pageable pageable); boolean existsByShopIdAndBarcode(Long shopId, String barcode); + + long countByShopIdAndBarcodeAndIdNot(Long shopId, String barcode, Long id); } diff --git a/backend/src/main/java/com/example/demo/product/service/ProductService.java b/backend/src/main/java/com/example/demo/product/service/ProductService.java index 7e79414..39a849c 100644 --- a/backend/src/main/java/com/example/demo/product/service/ProductService.java +++ b/backend/src/main/java/com/example/demo/product/service/ProductService.java @@ -160,6 +160,12 @@ public class ProductService { validate(shopId, req); Product p = productRepository.findById(id).orElseThrow(); if (!p.getShopId().equals(shopId)) throw new IllegalArgumentException("跨店铺数据"); + // 条码唯一性:允许与自身相同,但不允许与其他商品重复 + if (req.barcode != null && !req.barcode.isBlank()) { + if (productRepository.countByShopIdAndBarcodeAndIdNot(shopId, req.barcode, id) > 0) { + throw new IllegalArgumentException("条码已存在"); + } + } p.setUserId(userId); p.setName(req.name); p.setBarcode(emptyToNull(req.barcode)); diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index cfc98fc..cee7bf4 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -39,6 +39,19 @@ spring.datasource.hikari.connection-timeout=30000 attachments.placeholder.image-path=${ATTACHMENTS_PLACEHOLDER_IMAGE} attachments.placeholder.url-path=${ATTACHMENTS_PLACEHOLDER_URL:/api/attachments/placeholder} +# 纯URL引用校验配置(方案A) +attachments.url.ssrf-protection=${ATTACHMENTS_URL_SSRF_PROTECTION:true} +attachments.url.allow-private-ip=${ATTACHMENTS_URL_ALLOW_PRIVATE_IP:false} +attachments.url.follow-redirects=${ATTACHMENTS_URL_FOLLOW_REDIRECTS:true} +attachments.url.max-redirects=${ATTACHMENTS_URL_MAX_REDIRECTS:2} +attachments.url.connect-timeout-ms=${ATTACHMENTS_URL_CONNECT_TIMEOUT_MS:3000} +attachments.url.read-timeout-ms=${ATTACHMENTS_URL_READ_TIMEOUT_MS:5000} +attachments.url.max-size-mb=${ATTACHMENTS_URL_MAX_SIZE_MB:5} +# 逗号分隔域名,支持前缀通配 *.example.com +attachments.url.allowlist=${ATTACHMENTS_URL_ALLOWLIST:} +# 逗号分隔Content-Type +attachments.url.allowed-content-types=${ATTACHMENTS_URL_ALLOWED_CONTENT_TYPES:image/jpeg,image/png,image/gif,image/webp,image/svg+xml} + # 应用默认上下文(用于开发/演示环境) app.defaults.shop-id=${APP_DEFAULT_SHOP_ID:1} app.defaults.user-id=${APP_DEFAULT_USER_ID:2} diff --git a/doc/database_documentation.md b/doc/database_documentation.md index f950f43..a7dee97 100644 --- a/doc/database_documentation.md +++ b/doc/database_documentation.md @@ -14,7 +14,12 @@ | updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | | deleted_at | DATETIME | YES | | | -**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_shops_status` (`status`) +字段说明: +- id: 主键,自增 +- name: 店铺名称 +- status: 店铺状态(1启用/0停用) +- created_at/updated_at: 创建/更新时间 +- deleted_at: 逻辑删除时间 ### users | Column Name | Data Type | Nullable | Default | Comment | @@ -31,8 +36,11 @@ | updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | | deleted_at | DATETIME | YES | | | -**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_users_shop` (`shop_id`) - UNIQUE: `ux_users_shop_phone` (`shop_id`,`phone`) -**Foreign Keys**: - `fk_users_shop`: `shop_id` → `shops(id)` ON UPDATE NO ACTION ON DELETE NO ACTION +字段说明: +- shop_id: 归属店铺 +- role: 角色标识字符串 +- is_owner: 是否店主标记 +- 其余同名含义 ### user_identities | Column Name | Data Type | Nullable | Default | Comment | @@ -49,8 +57,9 @@ | created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | | updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | -**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_identity_shop` (`shop_id`) - KEY: `idx_identity_user` (`user_id`) - UNIQUE: `ux_identity_provider_openid` (`provider`,`openid`) - UNIQUE: `ux_identity_unionid` (`unionid`) -**Foreign Keys**: - `fk_identity_shop`: `shop_id` → `shops(id)` ON UPDATE NO ACTION ON DELETE NO ACTION - `fk_identity_user`: `user_id` → `users(id)` ON UPDATE NO ACTION ON DELETE NO ACTION +字段说明: +- provider: wechat_mp(小程序)、wechat_app(APP) +- openid/unionid: 微信身份标识 ### wechat_sessions | Column Name | Data Type | Nullable | Default | Comment | @@ -62,7 +71,8 @@ | expires_at | DATETIME | NOT NULL | | | | created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | -**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_wechat_session_expires` (`expires_at`) - UNIQUE: `ux_wechat_session` (`provider`,`openid`) +字段说明: +- session_key/expires_at: 会话密钥与过期时间 ### system_parameters | Column Name | Data Type | Nullable | Default | Comment | @@ -75,8 +85,8 @@ | created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | | updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | -**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_sysparams_shop` (`shop_id`) - UNIQUE: `ux_sysparams_shop_key` (`shop_id`,`key`) -**Foreign Keys**: - `fk_sysparams_shop`: `shop_id` → `shops(id)` ON UPDATE NO ACTION ON DELETE NO ACTION - `fk_sysparams_user`: `user_id` → `users(id)` ON UPDATE NO ACTION ON DELETE NO ACTION +字段说明: +- key/value: 键/值(JSON) ### product_units | Column Name | Data Type | Nullable | Default | Comment | @@ -89,6 +99,9 @@ | updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | | deleted_at | DATETIME | YES | | | +字段说明(product_units): +- name: 单位名称,如 件/个/箱 + **Indexes**: - PRIMARY KEY: `id` - KEY: `idx_units_shop` (`shop_id`) - UNIQUE: `ux_units_shop_name` (`shop_id`,`name`) **Foreign Keys**: - `fk_units_shop`: `shop_id` → `shops(id)` ON UPDATE NO ACTION ON DELETE NO ACTION - `fk_units_user`: `user_id` → `users(id)` ON UPDATE NO ACTION ON DELETE NO ACTION @@ -108,6 +121,12 @@ | updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | | deleted_at | DATETIME | YES | | | +字段说明(global_skus): +- name/brand/model/spec/barcode: SKU 基本属性 +- unit_id: 对应的计量单位 +- tags: 结构化标签 JSON +- status: 上架状态(published/offline) + **Indexes**: - PRIMARY KEY: `id` - KEY: `idx_global_skus_brand_model` (`brand`,`model`) - UNIQUE: `ux_global_skus_barcode` (`barcode`) **Foreign Keys**: - `fk_globalsku_unit`: `unit_id` → `product_units(id)` ON UPDATE NO ACTION ON DELETE NO ACTION @@ -124,6 +143,10 @@ | updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | | deleted_at | DATETIME | YES | | | +字段说明(product_categories): +- parent_id: 父分类,可为空 +- sort_order: 排序 + **Indexes**: - PRIMARY KEY: `id` - KEY: `idx_categories_shop` (`shop_id`) - KEY: `idx_categories_parent` (`parent_id`) - UNIQUE: `ux_categories_shop_name` (`shop_id`,`name`) **Foreign Keys**: - `fk_categories_shop`: `shop_id` → `shops(id)` ON UPDATE NO ACTION ON DELETE NO ACTION - `fk_categories_user`: `user_id` → `users(id)` ON UPDATE NO ACTION ON DELETE NO ACTION - `fk_categories_parent`: `parent_id` → `product_categories(id)` ON UPDATE NO ACTION ON DELETE NO ACTION @@ -151,6 +174,11 @@ | updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | | deleted_at | DATETIME | YES | | | +字段说明(products): +- category_id/unit_id/global_sku_id: 归属分类/单位/全局SKU +- safe_min/safe_max: 安全库存上下限 +- search_text: 聚合检索字段(触发器维护) + **Indexes**: - PRIMARY KEY: `id` - KEY: `idx_products_shop` (`shop_id`) - KEY: `idx_products_category` (`category_id`) - KEY: `idx_products_unit` (`unit_id`) - FULLTEXT: `ft_products_search` (`name`,`brand`,`model`,`spec`,`search_text`) - UNIQUE: `ux_products_shop_barcode` (`shop_id`,`barcode`) **Foreign Keys**: - `fk_products_shop`: `shop_id` → `shops(id)` - `fk_products_user`: `user_id` → `users(id)` - `fk_products_category`: `category_id` → `product_categories(id)` - `fk_products_unit`: `unit_id` → `product_units(id)` - `fk_products_globalsku`: `global_sku_id` → `global_skus(id)` @@ -166,6 +194,9 @@ | updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | | deleted_at | DATETIME | YES | | | +字段说明(product_aliases): +- alias: 商品别名(同义词) + **Indexes**: - PRIMARY KEY: `id` - KEY: `idx_product_alias_product` (`product_id`) - UNIQUE: `ux_product_alias` (`product_id`,`alias`) **Foreign Keys**: - `fk_alias_shop`: `shop_id` → `shops(id)` - `fk_alias_user`: `user_id` → `users(id)` - `fk_alias_product`: `product_id` → `products(id)` @@ -181,6 +212,10 @@ | sort_order | INT | NOT NULL | 0 | | | created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | +字段说明(product_images): +- url/hash: 图片地址/内容哈希 +- sort_order: 展示顺序 + **Indexes**: - PRIMARY KEY: `id` - KEY: `idx_product_images_product` (`product_id`) - UNIQUE: `ux_product_image_hash` (`product_id`,`hash`) **Foreign Keys**: - `fk_pimg_shop`: `shop_id` → `shops(id)` - `fk_pimg_user`: `user_id` → `users(id)` - `fk_pimg_product`: `product_id` → `products(id)` ON DELETE CASCADE @@ -196,6 +231,10 @@ | big_client_price | DECIMAL(18,2) | NOT NULL | 0.00 | | | updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | +字段说明(product_prices): +- purchase_price: 当前进价(用于近似毛利) +- retail/wholesale/big_client_price: 售价列 + **Indexes**: - PRIMARY KEY: `product_id` - KEY: `idx_prices_shop` (`shop_id`) **Foreign Keys**: - `fk_prices_product`: `product_id` → `products(id)` ON DELETE CASCADE - `fk_prices_shop`: `shop_id` → `shops(id)` - `fk_prices_user`: `user_id` → `users(id)` @@ -208,6 +247,9 @@ | quantity | DECIMAL(18,3) | NOT NULL | 0.000 | | | updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | +字段说明(inventories): +- quantity: 当前库存数量(按商品一行聚合) + **Indexes**: - PRIMARY KEY: `product_id` - KEY: `idx_inventories_shop` (`shop_id`) **Foreign Keys**: - `fk_inv_product`: `product_id` → `products(id)` ON DELETE CASCADE - `fk_inv_shop`: `shop_id` → `shops(id)` - `fk_inv_user`: `user_id` → `users(id)` @@ -231,6 +273,11 @@ | updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | | deleted_at | DATETIME | YES | | | +字段说明(customers): +- level: 等级标签 +- price_level: 默认售价列(中文存储:零售价/批发价/大单报价) +- ar_opening: 期初应收 + **Indexes**: - PRIMARY KEY: `id` - KEY: `idx_customers_shop` (`shop_id`) - KEY: `idx_customers_phone` (`phone`) - KEY: `idx_customers_mobile` (`mobile`) **Foreign Keys**: - `fk_customers_shop`: `shop_id` → `shops(id)` - `fk_customers_user`: `user_id` → `users(id)` @@ -253,6 +300,9 @@ | updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | | deleted_at | DATETIME | YES | | | +字段说明(suppliers): +- ap_opening/ap_payable: 期初应付/当前应付 + **Indexes**: - PRIMARY KEY: `id` - KEY: `idx_suppliers_shop` (`shop_id`) - KEY: `idx_suppliers_phone` (`phone`) - KEY: `idx_suppliers_mobile` (`mobile`) **Foreign Keys**: - `fk_suppliers_shop`: `shop_id` → `shops(id)` - `fk_suppliers_user`: `user_id` → `users(id)` @@ -272,6 +322,10 @@ | updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | | deleted_at | DATETIME | YES | | | +字段说明(accounts): +- type: 账户类型(cash/bank/alipay/wechat/other) +- bank_name/bank_account: 银行账户信息(type=bank 时使用) + **Indexes**: - PRIMARY KEY: `id` - KEY: `idx_accounts_shop` (`shop_id`) - UNIQUE: `ux_accounts_shop_name` (`shop_id`,`name`) **Foreign Keys**: - `fk_accounts_shop`: `shop_id` → `shops(id)` - `fk_accounts_user`: `user_id` → `users(id)` @@ -292,6 +346,10 @@ | updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | | deleted_at | DATETIME | YES | | | +字段说明(sales_orders): +- status: 单据状态(draft/approved/returned/void) +- amount/paid_amount: 应收/已收合计 + **Indexes**: - PRIMARY KEY: `id` - KEY: `idx_sales_shop_time` (`shop_id`,`order_time`) - KEY: `idx_sales_customer` (`customer_id`) - UNIQUE: `ux_sales_order_no` (`shop_id`,`order_no`) **Foreign Keys**: - `fk_sales_shop`: `shop_id` → `shops(id)` - `fk_sales_user`: `user_id` → `users(id)` - `fk_sales_customer`: `customer_id` → `customers(id)` @@ -306,6 +364,9 @@ | discount_rate | DECIMAL(5,2) | NOT NULL | 0.00 | 折扣百分比0-100 | | amount | DECIMAL(18,2) | NOT NULL | | | +字段说明(sales_order_items): +- quantity/unit_price/discount_rate/amount: 数量/单价/折扣%/行金额 + **Indexes**: - PRIMARY KEY: `id` - KEY: `idx_soi_order` (`order_id`) - KEY: `idx_soi_product` (`product_id`) **Foreign Keys**: - `fk_soi_order`: `order_id` → `sales_orders(id)` ON DELETE CASCADE - `fk_soi_product`: `product_id` → `products(id)` @@ -326,6 +387,9 @@ | updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | | deleted_at | DATETIME | YES | | | +字段说明(purchase_orders/purchase_order_items): +- 与销售单结构类似,含应付与明细 + **Indexes**: - PRIMARY KEY: `id` - KEY: `idx_purchase_shop_time` (`shop_id`,`order_time`) - KEY: `idx_purchase_supplier` (`supplier_id`) - UNIQUE: `ux_purchase_order_no` (`shop_id`,`order_no`) **Foreign Keys**: - `fk_purchase_shop`: `shop_id` → `shops(id)` - `fk_purchase_user`: `user_id` → `users(id)` - `fk_purchase_supplier`: `supplier_id` → `suppliers(id)` @@ -357,6 +421,11 @@ | remark | VARCHAR(255) | YES | | | | created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | +字段说明(payments): +- biz_type/biz_id: 业务来源及关联主键 +- direction: in 收款 / out 付款 +- account_id: 使用的结算账户 + **Indexes**: - PRIMARY KEY: `id` - KEY: `idx_payments_shop_time` (`shop_id`,`pay_time`) - KEY: `idx_payments_biz` (`biz_type`,`biz_id`) **Foreign Keys**: - `fk_payments_shop`: `shop_id` → `shops(id)` - `fk_payments_user`: `user_id` → `users(id)` - `fk_payments_account`: `account_id` → `accounts(id)` @@ -378,6 +447,10 @@ | updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | | deleted_at | DATETIME | YES | | | +字段说明(other_transactions): +- type/category: 收入/支出与分类 +- counterparty_type/id: 往来单位(可空) + **Indexes**: - PRIMARY KEY: `id` - KEY: `idx_ot_shop_time` (`shop_id`,`tx_time`) - KEY: `idx_ot_account` (`account_id`) **Foreign Keys**: - `fk_ot_shop`: `shop_id` → `shops(id)` - `fk_ot_user`: `user_id` → `users(id)` - `fk_ot_account`: `account_id` → `accounts(id)` @@ -394,6 +467,10 @@ | created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | | updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | +字段说明(finance_categories): +- key: 稳定标识(代码不随展示文案改变) +- label: 展示名称 + **Indexes**: - PRIMARY KEY: `id` - UNIQUE: `ux_finance_cat` (`shop_id`,`type`,`key`) - KEY: `idx_finance_cat_shop_type` (`shop_id`,`type`) **Foreign Keys**: - `fk_finance_cat_shop`: `shop_id` → `shops(id)` @@ -420,6 +497,11 @@ | updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | | deleted_at | DATETIME | YES | | | +字段说明(notices): +- is_pinned: 是否置顶 +- starts_at/ends_at: 生效时间窗 +- status: 草稿/发布/下线 + **Indexes**: - PRIMARY KEY: `id` - KEY: `idx_notices_time` (`starts_at`,`ends_at`) **Foreign Keys**: - 无 @@ -439,6 +521,11 @@ | remark | VARCHAR(255) | YES | | | | created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | +字段说明(inventory_movements): +- qty_delta: 数量变动(入库为正、出库为负) +- amount_delta: 金额变动(可选) +- source_type/source_id: 变动来源追溯 + **Indexes**: - PRIMARY KEY: `id` - KEY: `idx_im_shop_time` (`shop_id`,`tx_time`) - KEY: `idx_im_product` (`product_id`) **Foreign Keys**: - `fk_im_shop`: `shop_id` → `shops(id)` - `fk_im_user`: `user_id` → `users(id)` - `fk_im_product`: `product_id` → `products(id)` @@ -466,6 +553,9 @@ | updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | | deleted_at | DATETIME | YES | | | +字段说明(sales_return_orders): +- 与销售单结构一致,用于退货业务;状态为 approved/void + **Indexes**: - PRIMARY KEY: `id` - UNIQUE: `ux_sr_order_no` (`shop_id`,`order_no`) - KEY: `idx_sr_shop_time` (`shop_id`,`order_time`) **Foreign Keys**: - `fk_sr_shop`: `shop_id` → `shops(id)` - `fk_sr_user`: `user_id` → `users(id)` - `fk_sr_customer`: `customer_id` → `customers(id)` @@ -500,6 +590,9 @@ | updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | | deleted_at | DATETIME | YES | | | +字段说明(purchase_return_orders): +- 与销售单结构一致,用于退货业务;状态为 approved/void + **Indexes**: - PRIMARY KEY: `id` - UNIQUE: `ux_pr_order_no` (`shop_id`,`order_no`) - KEY: `idx_pr_shop_time` (`shop_id`,`order_time`) **Foreign Keys**: - `fk_pr_shop`: `shop_id` → `shops(id)` - `fk_pr_user`: `user_id` → `users(id)` - `fk_pr_supplier`: `supplier_id` → `suppliers(id)` diff --git a/doc/openapi.yaml b/doc/openapi.yaml index 5aecbc9..d3da228 100644 --- a/doc/openapi.yaml +++ b/doc/openapi.yaml @@ -876,8 +876,8 @@ paths: direction: { type: string } /api/attachments: post: - summary: 上传附件(✅ Fully Implemented,占位图方案) - description: 接收 multipart 上传但忽略文件内容,始终返回占位图 URL(后端配置项 `attachments.placeholder.image-path` 指向本地占位图片;URL 固定 `/api/attachments/placeholder` 可通过 `attachments.placeholder.url-path` 覆盖)。 + summary: 上传附件(❌ Partially Implemented,已废弃:采用纯URL引用) + description: 方案A采用纯URL引用,不再支持文件直传。历史兼容:此接口仍存在但返回占位图 URL,建议前端改用 `/api/attachments/validate-url` 进行URL校验后直接写入业务字段。 requestBody: required: true content: @@ -902,6 +902,36 @@ paths: properties: url: type: string + /api/attachments/validate-url: + post: + summary: 校验图片URL(✅ Fully Implemented) + description: 提交外部图片URL,后端进行白名单、SSR防护、类型与大小校验,返回标准化后的可用URL和元信息。支持 `application/json` 与 `application/x-www-form-urlencoded`。 + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + url: { type: string } + required: [url] + application/x-www-form-urlencoded: + schema: + type: object + properties: + url: { type: string } + required: [url] + responses: + '200': + description: 成功 + content: + application/json: + schema: + type: object + properties: + url: { type: string } + contentType: { type: string } + contentLength: { type: integer, format: int64, nullable: true } /api/attachments/placeholder: get: summary: 附件占位图读取(✅ Fully Implemented) diff --git a/frontend/main.js b/frontend/main.js index f44f1b6..5b0e322 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -44,4 +44,24 @@ if (typeof uni !== 'undefined' && typeof uni.connectSocket === 'function') { return task } } +// #endif + +// 全局安全返回:首屏无法后退时自动回到首页 tab(微信小程序) +// #ifdef MP-WEIXIN +if (typeof uni !== 'undefined' && typeof uni.navigateBack === 'function') { + const _navigateBack = uni.navigateBack + uni.navigateBack = function(params = {}) { + try { + const pages = typeof getCurrentPages === 'function' ? getCurrentPages() : [] + const maxDelta = pages.length > 0 ? (pages.length - 1) : 0 + const d = Number(params.delta || 1) + if (maxDelta >= 1 && d <= maxDelta) { + return _navigateBack.call(this, params) + } + return uni.switchTab({ url: '/pages/index/index' }) + } catch (e) { + return uni.switchTab({ url: '/pages/index/index' }) + } + } +} // #endif \ No newline at end of file diff --git a/frontend/unpackage/dist/dev/.sourcemap/mp-weixin/app.js.map b/frontend/unpackage/dist/dev/.sourcemap/mp-weixin/app.js.map index c00f41f..d14201d 100644 --- a/frontend/unpackage/dist/dev/.sourcemap/mp-weixin/app.js.map +++ b/frontend/unpackage/dist/dev/.sourcemap/mp-weixin/app.js.map @@ -1 +1 @@ -{"version":3,"file":"app.js","sources":["App.vue","main.js"],"sourcesContent":["\"D:\\wx\\PartsInquiry\\frontend\\static\\icons\\icons8-account-male-100.png\"\r\n\r\n\r\n","import App from './App'\r\n\r\n// #ifndef VUE3\r\nimport Vue from 'vue'\r\nimport './uni.promisify.adaptor'\r\nVue.config.productionTip = false\r\nApp.mpType = 'app'\r\nconst app = new Vue({\r\n ...App\r\n})\r\napp.$mount()\r\n// #endif\r\n\r\n// #ifdef VUE3\r\nimport { createSSRApp } from 'vue'\r\nexport function createApp() {\r\n const app = createSSRApp(App)\r\n return {\r\n app\r\n }\r\n}\r\n// #endif\r\n\r\n// 规范化 WebSocket 关闭码(仅微信小程序)\r\n// #ifdef MP-WEIXIN\r\nif (typeof uni !== 'undefined' && typeof uni.connectSocket === 'function') {\r\n const _connectSocket = uni.connectSocket\r\n uni.connectSocket = function(options) {\r\n const task = _connectSocket.call(this, options)\r\n if (task && typeof task.close === 'function') {\r\n const _close = task.close\r\n task.close = function(params = {}) {\r\n if (params && typeof params === 'object') {\r\n const codeNum = Number(params.code)\r\n const isValid = codeNum === 1000 || (codeNum >= 3000 && codeNum <= 4999)\r\n if (!isValid) {\r\n params.code = 1000\r\n if (!params.reason) params.reason = 'normalized from invalid close code'\r\n }\r\n }\r\n return _close.call(this, params)\r\n }\r\n }\r\n return task\r\n }\r\n}\r\n// #endif"],"names":["uni","createSSRApp","App"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AACC,MAAK,YAAU;AAAA,EACd,UAAU,WAAW;AACpBA,kBAAAA,MAAA,MAAA,OAAA,gBAAY,YAAY;AAAA,EACxB;AAAA,EACD,QAAQ,WAAW;AAClBA,kBAAAA,MAAY,MAAA,OAAA,gBAAA,UAAU;AAAA,EACtB;AAAA,EACD,QAAQ,WAAW;AAClBA,kBAAAA,MAAY,MAAA,OAAA,iBAAA,UAAU;AAAA,EACvB;AACD;ACIM,SAAS,YAAY;AAC1B,QAAM,MAAMC,cAAY,aAACC,SAAG;AAC5B,SAAO;AAAA,IACL;AAAA,EACD;AACH;AAKA,IAAI,OAAOF,cAAG,UAAK,eAAe,OAAOA,cAAAA,MAAI,kBAAkB,YAAY;AACzE,QAAM,iBAAiBA,cAAAA,MAAI;AAC3BA,sBAAI,gBAAgB,SAAS,SAAS;AACpC,UAAM,OAAO,eAAe,KAAK,MAAM,OAAO;AAC9C,QAAI,QAAQ,OAAO,KAAK,UAAU,YAAY;AAC5C,YAAM,SAAS,KAAK;AACpB,WAAK,QAAQ,SAAS,SAAS,IAAI;AACjC,YAAI,UAAU,OAAO,WAAW,UAAU;AACxC,gBAAM,UAAU,OAAO,OAAO,IAAI;AAClC,gBAAM,UAAU,YAAY,OAAS,WAAW,OAAQ,WAAW;AACnE,cAAI,CAAC,SAAS;AACZ,mBAAO,OAAO;AACd,gBAAI,CAAC,OAAO;AAAQ,qBAAO,SAAS;AAAA,UACrC;AAAA,QACF;AACD,eAAO,OAAO,KAAK,MAAM,MAAM;AAAA,MAChC;AAAA,IACF;AACD,WAAO;AAAA,EACR;AACH;;;"} \ No newline at end of file +{"version":3,"file":"app.js","sources":["App.vue","main.js"],"sourcesContent":["\"D:\\wx\\PartsInquiry\\frontend\\static\\icons\\icons8-account-male-100.png\"\r\n\r\n\r\n","import App from './App'\r\n\r\n// #ifndef VUE3\r\nimport Vue from 'vue'\r\nimport './uni.promisify.adaptor'\r\nVue.config.productionTip = false\r\nApp.mpType = 'app'\r\nconst app = new Vue({\r\n ...App\r\n})\r\napp.$mount()\r\n// #endif\r\n\r\n// #ifdef VUE3\r\nimport { createSSRApp } from 'vue'\r\nexport function createApp() {\r\n const app = createSSRApp(App)\r\n return {\r\n app\r\n }\r\n}\r\n// #endif\r\n\r\n// 规范化 WebSocket 关闭码(仅微信小程序)\r\n// #ifdef MP-WEIXIN\r\nif (typeof uni !== 'undefined' && typeof uni.connectSocket === 'function') {\r\n const _connectSocket = uni.connectSocket\r\n uni.connectSocket = function(options) {\r\n const task = _connectSocket.call(this, options)\r\n if (task && typeof task.close === 'function') {\r\n const _close = task.close\r\n task.close = function(params = {}) {\r\n if (params && typeof params === 'object') {\r\n const codeNum = Number(params.code)\r\n const isValid = codeNum === 1000 || (codeNum >= 3000 && codeNum <= 4999)\r\n if (!isValid) {\r\n params.code = 1000\r\n if (!params.reason) params.reason = 'normalized from invalid close code'\r\n }\r\n }\r\n return _close.call(this, params)\r\n }\r\n }\r\n return task\r\n }\r\n}\r\n// #endif\r\n\r\n// 全局安全返回:首屏无法后退时自动回到首页 tab(微信小程序)\r\n// #ifdef MP-WEIXIN\r\nif (typeof uni !== 'undefined' && typeof uni.navigateBack === 'function') {\r\n const _navigateBack = uni.navigateBack\r\n uni.navigateBack = function(params = {}) {\r\n try {\r\n const pages = typeof getCurrentPages === 'function' ? getCurrentPages() : []\r\n const maxDelta = pages.length > 0 ? (pages.length - 1) : 0\r\n const d = Number(params.delta || 1)\r\n if (maxDelta >= 1 && d <= maxDelta) {\r\n return _navigateBack.call(this, params)\r\n }\r\n return uni.switchTab({ url: '/pages/index/index' })\r\n } catch (e) {\r\n return uni.switchTab({ url: '/pages/index/index' })\r\n }\r\n }\r\n}\r\n// #endif"],"names":["uni","createSSRApp","App"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AACC,MAAK,YAAU;AAAA,EACd,UAAU,WAAW;AACpBA,kBAAAA,MAAA,MAAA,OAAA,gBAAY,YAAY;AAAA,EACxB;AAAA,EACD,QAAQ,WAAW;AAClBA,kBAAAA,MAAY,MAAA,OAAA,gBAAA,UAAU;AAAA,EACtB;AAAA,EACD,QAAQ,WAAW;AAClBA,kBAAAA,MAAY,MAAA,OAAA,iBAAA,UAAU;AAAA,EACvB;AACD;ACIM,SAAS,YAAY;AAC1B,QAAM,MAAMC,cAAY,aAACC,SAAG;AAC5B,SAAO;AAAA,IACL;AAAA,EACD;AACH;AAKA,IAAI,OAAOF,cAAG,UAAK,eAAe,OAAOA,cAAAA,MAAI,kBAAkB,YAAY;AACzE,QAAM,iBAAiBA,cAAAA,MAAI;AAC3BA,sBAAI,gBAAgB,SAAS,SAAS;AACpC,UAAM,OAAO,eAAe,KAAK,MAAM,OAAO;AAC9C,QAAI,QAAQ,OAAO,KAAK,UAAU,YAAY;AAC5C,YAAM,SAAS,KAAK;AACpB,WAAK,QAAQ,SAAS,SAAS,IAAI;AACjC,YAAI,UAAU,OAAO,WAAW,UAAU;AACxC,gBAAM,UAAU,OAAO,OAAO,IAAI;AAClC,gBAAM,UAAU,YAAY,OAAS,WAAW,OAAQ,WAAW;AACnE,cAAI,CAAC,SAAS;AACZ,mBAAO,OAAO;AACd,gBAAI,CAAC,OAAO;AAAQ,qBAAO,SAAS;AAAA,UACrC;AAAA,QACF;AACD,eAAO,OAAO,KAAK,MAAM,MAAM;AAAA,MAChC;AAAA,IACF;AACD,WAAO;AAAA,EACR;AACH;AAKA,IAAI,OAAOA,cAAG,UAAK,eAAe,OAAOA,cAAAA,MAAI,iBAAiB,YAAY;AACxE,QAAM,gBAAgBA,cAAAA,MAAI;AAC1BA,gBAAAA,MAAI,eAAe,SAAS,SAAS,IAAI;AACvC,QAAI;AACF,YAAM,QAAQ,OAAO,oBAAoB,aAAa,gBAAiB,IAAG,CAAE;AAC5E,YAAM,WAAW,MAAM,SAAS,IAAK,MAAM,SAAS,IAAK;AACzD,YAAM,IAAI,OAAO,OAAO,SAAS,CAAC;AAClC,UAAI,YAAY,KAAK,KAAK,UAAU;AAClC,eAAO,cAAc,KAAK,MAAM,MAAM;AAAA,MACvC;AACD,aAAOA,cAAG,MAAC,UAAU,EAAE,KAAK,qBAAoB,CAAE;AAAA,IACnD,SAAQ,GAAG;AACV,aAAOA,cAAG,MAAC,UAAU,EAAE,KAAK,qBAAoB,CAAE;AAAA,IACnD;AAAA,EACF;AACH;;;"} \ No newline at end of file diff --git a/frontend/unpackage/dist/dev/.sourcemap/mp-weixin/components/ImageUploader.js.map b/frontend/unpackage/dist/dev/.sourcemap/mp-weixin/components/ImageUploader.js.map index 9756304..803e917 100644 --- a/frontend/unpackage/dist/dev/.sourcemap/mp-weixin/components/ImageUploader.js.map +++ b/frontend/unpackage/dist/dev/.sourcemap/mp-weixin/components/ImageUploader.js.map @@ -1 +1 @@ -{"version":3,"file":"ImageUploader.js","sources":["components/ImageUploader.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniComponent:/QzovVXNlcnMvMjE4MjYvRGVza3RvcC9Xai9QYXJ0c0lucXVpcnkvZnJvbnRlbmQvY29tcG9uZW50cy9JbWFnZVVwbG9hZGVyLnZ1ZQ"],"sourcesContent":["\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n","import Component from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/components/ImageUploader.vue'\nwx.createComponent(Component)"],"names":["uni","upload"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"ImageUploader.js","sources":["components/ImageUploader.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniComponent:/QzovVXNlcnMvMjE4MjYvRGVza3RvcC9Xai9QYXJ0c0lucXVpcnkvZnJvbnRlbmQvY29tcG9uZW50cy9JbWFnZVVwbG9hZGVyLnZ1ZQ"],"sourcesContent":["\n\n\n\n\n\n\n\n\n\n\n\n","import Component from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/components/ImageUploader.vue'\nwx.createComponent(Component)"],"names":["uni","upload"],"mappings":"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"} \ No newline at end of file diff --git a/frontend/unpackage/dist/dev/mp-weixin/app.js b/frontend/unpackage/dist/dev/mp-weixin/app.js index 07f557a..12e36e4 100644 --- a/frontend/unpackage/dist/dev/mp-weixin/app.js +++ b/frontend/unpackage/dist/dev/mp-weixin/app.js @@ -62,6 +62,22 @@ if (typeof common_vendor.index !== "undefined" && typeof common_vendor.index.con return task; }; } +if (typeof common_vendor.index !== "undefined" && typeof common_vendor.index.navigateBack === "function") { + const _navigateBack = common_vendor.index.navigateBack; + common_vendor.index.navigateBack = function(params = {}) { + try { + const pages = typeof getCurrentPages === "function" ? getCurrentPages() : []; + const maxDelta = pages.length > 0 ? pages.length - 1 : 0; + const d = Number(params.delta || 1); + if (maxDelta >= 1 && d <= maxDelta) { + return _navigateBack.call(this, params); + } + return common_vendor.index.switchTab({ url: "/pages/index/index" }); + } catch (e) { + return common_vendor.index.switchTab({ url: "/pages/index/index" }); + } + }; +} createApp().app.mount("#app"); exports.createApp = createApp; //# sourceMappingURL=../.sourcemap/mp-weixin/app.js.map diff --git a/沟通.md b/沟通.md index 36af692..1c3780a 100644 --- a/沟通.md +++ b/沟通.md @@ -1,52 +1,3 @@ -9.16王德鹏1: -前端:1.公告:删除示例,删除“更多”,更改为“广告” - 2.调整核心数据概览内容(根据需求文档) - 3.导航栏内容改为“首页”“货品”“开单”“明细”“报表”“我的”(根据需求文档) - 4.开单功能√ -后端:1.公告的显示功能√ - 2.默认用户张老板,id=2 -数据库:新增notice表/所有表填充模拟数据 -另:doc里有数据库文档和需求文档 - -9.17王德鹏1: -前端:1.货品功能√ -后端:1.首页核心数据√ - 2.所有图片用占位图backend\picture代替 - -9.18王德鹏1: -前端:1.明细功能√ -后端:1.数据库驱动改为硬编码 - 2.货品功能√ - -9.18王德鹏2: -前端:1.客户功能√ -数据库:1.客户表更改 - -9.18王德鹏3: -前端:1.供应商功能√ -后端:1.客户功能√ - 2.供应商功能√v - 3.开单功能(销售进货)√ - -9.20王德鹏1: -前端:1.账户功能√ - 2.我的功能√ - 3.报表功能√ -后端:1.账户功能√ - 2.开单功能√ - 3.明细功能√ - 4.报表功能√ - -9.20王德鹏2: -除了“我的”,正常功能基本实现 - -9.20王德鹏3: -导航栏持久化 - - - - - 前端问题: 1.都只做了小程序的,就没看过安卓端,还需要做双端适配 2.图标 @@ -60,5 +11,5 @@ 5.缓存问题 6.“我的”没做 7.vip功能模糊 -8.管理端没做 -9.咨询功能没做 +8.管理端功能模糊 +9.咨询功能模糊