增加微信支付,商城逻辑,公告
This commit is contained in:
		
							
								
								
									
										18
									
								
								.env
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								.env
									
									
									
									
									
								
							| @@ -44,3 +44,21 @@ MINIO_BUCKET_UPLOADS=jurongquan | |||||||
| MINIO_BUCKET_AVATARS=jurongquan | MINIO_BUCKET_AVATARS=jurongquan | ||||||
| MINIO_BUCKET_PRODUCTS=jurongquan | MINIO_BUCKET_PRODUCTS=jurongquan | ||||||
| MINIO_BUCKET_DOCUMENTS=jurongquan | MINIO_BUCKET_DOCUMENTS=jurongquan | ||||||
|  |  | ||||||
|  | #支付配置 | ||||||
|  | WECHAT_APP_ID=wx3a702dbe13fd2217 | ||||||
|  | WECHAT_MCH_ID=1726377336 | ||||||
|  | WECHAT_API_KEY=NINGBOJURONGkejiyouxiangongsi202 | ||||||
|  | WECHAT_API_V3_KEY=NINGBOJURONGkejiyouxiangongsi202 | ||||||
|  | WECHAT_CERT_PATH=./cert/apiclient_cert.pem | ||||||
|  | WECHAT_KEY_PATH=./cert/apiclient_key.pem | ||||||
|  | WECHAT_NOTIFY_URL=https://www.zrbjr.com/api/wechat-pay/notify | ||||||
|  |  | ||||||
|  | # 支付宝配置 | ||||||
|  | # 请在支付宝开放平台获取以下配置信息: | ||||||
|  | # 1. 应用ID:在支付宝开放平台创建应用后获得 | ||||||
|  | # 2. 应用私钥和支付宝公钥现在从文件读取 | ||||||
|  | ALIPAY_APP_ID=2021005188682022 | ||||||
|  | ALIPAY_NOTIFY_URL=https://www.zrbjr.com/api/payment/alipay/notify | ||||||
|  | ALIPAY_RETURN_URL=https://www.zrbjr.com/payment/success | ||||||
|  | ALIPAY_QUIT_URL=https://www.zrbjr.com/payment/cancel | ||||||
							
								
								
									
										17
									
								
								.env.example
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								.env.example
									
									
									
									
									
								
							| @@ -41,3 +41,20 @@ MINIO_BUCKET_UPLOADS=jurongquan | |||||||
| MINIO_BUCKET_AVATARS=jurongquan | MINIO_BUCKET_AVATARS=jurongquan | ||||||
| MINIO_BUCKET_PRODUCTS=jurongquan | MINIO_BUCKET_PRODUCTS=jurongquan | ||||||
| MINIO_BUCKET_DOCUMENTS=jurongquan | MINIO_BUCKET_DOCUMENTS=jurongquan | ||||||
|  |  | ||||||
|  | # 微信支付配置 | ||||||
|  | WECHAT_APP_ID=your_wechat_app_id | ||||||
|  | WECHAT_MCH_ID=your_wechat_mch_id | ||||||
|  | WECHAT_API_KEY=your_wechat_api_key | ||||||
|  | WECHAT_API_V3_KEY=your_wechat_api_v3_key | ||||||
|  | WECHAT_NOTIFY_URL=https://your-domain.com/api/wechat-pay/notify | ||||||
|  | WECHAT_CERT_PATH=./cert/apiclient_cert.pem | ||||||
|  | WECHAT_KEY_PATH=./cert/apiclient_key.pem | ||||||
|  |  | ||||||
|  | # 支付宝支付配置 | ||||||
|  | ALIPAY_APP_ID=your_alipay_app_id | ||||||
|  | ALIPAY_PRIVATE_KEY=your_alipay_private_key | ||||||
|  | ALIPAY_PUBLIC_KEY=your_alipay_public_key | ||||||
|  | ALIPAY_GATEWAY_URL=https://openapi.alipay.com/gateway.do | ||||||
|  | ALIPAY_NOTIFY_URL=https://your-domain.com/api/alipay/notify | ||||||
|  | ALIPAY_RETURN_URL=https://your-domain.com/payment/success | ||||||
|   | |||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								cert/apiclient_cert.p12
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								cert/apiclient_cert.p12
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										25
									
								
								cert/apiclient_cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								cert/apiclient_cert.pem
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | -----BEGIN CERTIFICATE----- | ||||||
|  | MIIEKDCCAxCgAwIBAgIUXysc+VIXyKce5hqbA2vg9d9uH7kwDQYJKoZIhvcNAQEL | ||||||
|  | BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT | ||||||
|  | FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg | ||||||
|  | Q0EwHhcNMjUwOTAyMDg0NDA0WhcNMzAwOTAxMDg0NDA0WjCBgTETMBEGA1UEAwwK | ||||||
|  | MTcyNjM3NzMzNjEbMBkGA1UECgwS5b6u5L+h5ZWG5oi357O757ufMS0wKwYDVQQL | ||||||
|  | DCTlroHms6Lngqzono3mrYbliJvnp5HmioDmnInpmZDlhazlj7gxCzAJBgNVBAYT | ||||||
|  | AkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC | ||||||
|  | AQoCggEBAKncf/y0dT12nyCmnK5w3ECQgtoHrU32LqQH/N5SUbiJVbkys99Qq41D | ||||||
|  | MO0539Ki2MBuOuDHnWOrgNbRhH4WMXS8qodTcWu39cgVlnmw/KtQJdyInrPynFYH | ||||||
|  | lWO6boc1jfM4UEbEk90iaLM49uFo75+bCNGZVHOt6UMmTNFB9zHDfnhm+UaxNJOf | ||||||
|  | p1z7sntmD3H98c4ghzcE5L51KL6A7JtW+cp0zTpOunj0T/drvmRoyQVzqZ4IlXga | ||||||
|  | 9pJ/6Un2fVNn8pZL804ldKumAf5KVKmqEmz64ydh3896MoAshT/UJIHDuduN+Jbk | ||||||
|  | gROt9QiM68jR6CuqeRsV7EZ1OA/BLI8CAwEAAaOBuTCBtjAJBgNVHRMEAjAAMAsG | ||||||
|  | A1UdDwQEAwID+DCBmwYDVR0fBIGTMIGQMIGNoIGKoIGHhoGEaHR0cDovL2V2Y2Eu | ||||||
|  | aXRydXMuY29tLmNuL3B1YmxpYy9pdHJ1c2NybD9DQT0xQkQ0MjIwRTUwREJDMDRC | ||||||
|  | MDZBRDM5NzU0OTg0NkMwMUMzRThFQkQyJnNnPUhBQ0M0NzFCNjU0MjJFMTJCMjdB | ||||||
|  | OUQzM0E4N0FEMUNERjU5MjZFMTQwMzcxMA0GCSqGSIb3DQEBCwUAA4IBAQCzyJZ/ | ||||||
|  | +rnvUd1kJ74KCb6kxAwM/bX8w5lhkdUkeyQxdmbUCXCrkOGJ8uRQMfiK93eeET4h | ||||||
|  | KMrZywQHvL1E5WXQpUmQZVYj6eAxaUO+RoW8wPeWb5x/LbSXqQCrCNF6U+AvC6wj | ||||||
|  | 6haW8TK8egCLsjxPBXL9NjkxvcsIOIM8F6JKhMSAAjT7F1nkXthyxC50o/Mbox6l | ||||||
|  | YfvVZ70gWuUR2e6o9Sob1tTq6YwSwDr5OMwJT8QpDEdjAbWLLtv7a+ApzfqWeTHP | ||||||
|  | dmVawsaQnOzFaUPDtEGYSh23/eaTrJ9DHpgtubtU/CQsGos6QHD6huRYMmvw3Wml | ||||||
|  | mFAVWux4DJ8lZXGZ | ||||||
|  | -----END CERTIFICATE----- | ||||||
							
								
								
									
										28
									
								
								cert/apiclient_key.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								cert/apiclient_key.pem
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | -----BEGIN PRIVATE KEY----- | ||||||
|  | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCp3H/8tHU9dp8g | ||||||
|  | ppyucNxAkILaB61N9i6kB/zeUlG4iVW5MrPfUKuNQzDtOd/SotjAbjrgx51jq4DW | ||||||
|  | 0YR+FjF0vKqHU3Frt/XIFZZ5sPyrUCXciJ6z8pxWB5Vjum6HNY3zOFBGxJPdImiz | ||||||
|  | OPbhaO+fmwjRmVRzrelDJkzRQfcxw354ZvlGsTSTn6dc+7J7Zg9x/fHOIIc3BOS+ | ||||||
|  | dSi+gOybVvnKdM06Trp49E/3a75kaMkFc6meCJV4GvaSf+lJ9n1TZ/KWS/NOJXSr | ||||||
|  | pgH+SlSpqhJs+uMnYd/PejKALIU/1CSBw7nbjfiW5IETrfUIjOvI0egrqnkbFexG | ||||||
|  | dTgPwSyPAgMBAAECggEAF5dGN0Sg285zv0ckj52hGV54retPCHrec22gkwf/zY9V | ||||||
|  | VolSLfu4N8BTNT9KdKilTeSBTOKsW0FgfXVP32sZp0rkrDLMl9dOzWEiKviHvws8 | ||||||
|  | lupqkDdruw8Gknk8DI9FjbgOfiWjG51ByVJqB1hZn2Ma0HFpJz/KG8df99gfisuP | ||||||
|  | 3s4s/N8gChKRobNJzoFTVlEexVwM8vPZl7foxm9SEc0tbXyU2yB+pglVv/BkkLo5 | ||||||
|  | aZFNIC2TVnGX1WJlTTfuhng+MMeckSIPjJGH9c7RpgAChKGOBFOAK87/1+3tep47 | ||||||
|  | qp3i2WnceIxy2Y/wIT4cGWUAd0Ghi/kkIo8KqpagEQKBgQDfiwhBitymWc5QRr+R | ||||||
|  | LM6+Yu/EWlpd4/ZrcIgX9S5Z/La4gs/fqyWyKp9r/KjyYKG5sEfArim3NenvQBac | ||||||
|  | Vp99kNpjAl6SCSWFNeA7aY+zLDJBRHSaG4ovxWXk1ZYD2hBLKahzp7PCdcNvPICU | ||||||
|  | 60AapDMiGlfbcJDot1uP+9i16wKBgQDChiW9qroJGfZcpe5nkh38orPOp7or09mG | ||||||
|  | perNOAXGghJMV7tc1Se+ttjcvlfK6hrQcxPCMccH/x0bmeZ/MaG1RZjZS2zAXqH5 | ||||||
|  | 9huZtgIHc2idVS+j1t4HsszuIBp84c6ykCU+6NltW1LQFRZ9Y2HuA5HW/fgyhLMG | ||||||
|  | RHpRdgfG7QKBgDd9P5NlcNgqOrhal3rl8Hv5+yJ2ezALQkPxLxcjWVolDQZIEmmn | ||||||
|  | BjhvtBsOILHporuBMo51rQ05aNRmyDYOmpCEwHELSYZelt22Pe8BiRYkxmTFJVyL | ||||||
|  | sYWiLmTbT92s55aAxLvQySJgMR8Pmatdqg/y6m5ws5ZZHt9lhGj9TxH5AoGAR8Rc | ||||||
|  | WjyJxF/av9XMPlPvUkzoz76b9h2D7KR8G1im8NT+UUIw8xAFSNyG5/Ily8xRNkSu | ||||||
|  | rn/U8YNSxuMh4h16jrltqgWkythfJCyDhFNdLkiK+Tj7iZP1eJuj9drMSvS4YLLD | ||||||
|  | uxEHXsxJolGVaY9oCvswLESo9GJ29kH/atyEBAUCgYB+xm2NI9OpF+dSq5O8vMiW | ||||||
|  | NtlD744WfsiBDzatQz1A9PAB6cyLi03zjW10+HCNZRxBSfI8au/sn9lOdcCjvSGk | ||||||
|  | 8tjZXMzL71jDhEYs4qTUaiKJ2YpbLNcLFeTy48hWeRpew7t7bunksx8zb+0W14M2 | ||||||
|  | zDmIHi+CSve59dOzoiYuNg== | ||||||
|  | -----END PRIVATE KEY----- | ||||||
							
								
								
									
										18
									
								
								cert/证书使用说明.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								cert/证书使用说明.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | 欢迎使用微信支付! | ||||||
|  | 附件中的三份文件(证书pkcs12格式、证书pem格式、证书密钥pem格式),为接口中强制要求时需携带的证书文件。 | ||||||
|  | 证书属于敏感信息,请妥善保管不要泄露和被他人复制。 | ||||||
|  | 不同开发语言下的证书格式不同,以下为说明指引: | ||||||
|  |     证书pkcs12格式(apiclient_cert.p12) | ||||||
|  |         包含了私钥信息的证书文件,为p12(pfx)格式,由微信支付签发给您用来标识和界定您的身份 | ||||||
|  |         部分安全性要求较高的API需要使用该证书来确认您的调用身份 | ||||||
|  |         windows上可以直接双击导入系统,导入过程中会提示输入证书密码,证书密码默认为您的商户号(如:1900006031) | ||||||
|  |     证书pem格式(apiclient_cert.pem) | ||||||
|  |         从apiclient_cert.p12中导出证书部分的文件,为pem格式,请妥善保管不要泄漏和被他人复制 | ||||||
|  |         部分开发语言和环境,不能直接使用p12文件,而需要使用pem,所以为了方便您使用,已为您直接提供 | ||||||
|  |         您也可以使用openssl命令来自己导出:openssl pkcs12 -clcerts -nokeys -in apiclient_cert.p12 -out apiclient_cert.pem | ||||||
|  |     证书密钥pem格式(apiclient_key.pem) | ||||||
|  |         从apiclient_cert.p12中导出密钥部分的文件,为pem格式 | ||||||
|  |         部分开发语言和环境,不能直接使用p12文件,而需要使用pem,所以为了方便您使用,已为您直接提供 | ||||||
|  |         您也可以使用openssl命令来自己导出:openssl pkcs12 -nocerts -in apiclient_cert.p12 -out apiclient_key.pem | ||||||
|  | 备注说明:   | ||||||
|  |         由于绝大部分操作系统已内置了微信支付服务器证书的根CA证书,  2018年3月6日后, 不再提供CA证书文件(rootca.pem)下载  | ||||||
							
								
								
									
										34
									
								
								certs/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								certs/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | # 支付宝支付证书配置指南 | ||||||
|  |  | ||||||
|  | ## 证书文件说明 | ||||||
|  |  | ||||||
|  | 本目录需要包含以下证书文件,用于支付宝支付V3版本API的证书模式: | ||||||
|  |  | ||||||
|  | 1. `appCertPublicKey.crt` - 应用公钥证书(已存在) | ||||||
|  | 2. `alipayRootCert.crt` - 支付宝根证书(需下载) | ||||||
|  | 3. `alipayCertPublicKey_RSA2.crt` - 支付宝公钥证书(需下载) | ||||||
|  |  | ||||||
|  | ## 如何获取证书 | ||||||
|  |  | ||||||
|  | ### 已有证书 | ||||||
|  | - `appCertPublicKey.crt` - 应用公钥证书已存在于本目录 | ||||||
|  |  | ||||||
|  | ### 需要下载的证书 | ||||||
|  | 请登录支付宝开放平台,从您的应用详情页面下载以下证书: | ||||||
|  |  | ||||||
|  | 1. **支付宝根证书**:下载后重命名为 `alipayRootCert.crt` 并放入本目录 | ||||||
|  | 2. **支付宝公钥证书**:下载后重命名为 `alipayCertPublicKey_RSA2.crt` 并放入本目录 | ||||||
|  |  | ||||||
|  | ## 证书下载步骤 | ||||||
|  |  | ||||||
|  | 1. 登录[支付宝开放平台](https://open.alipay.com/) | ||||||
|  | 2. 进入「开发者中心」->「我的应用」 | ||||||
|  | 3. 选择对应的应用 | ||||||
|  | 4. 点击「开发设置」 | ||||||
|  | 5. 在「接口加签方式」区域,选择「公钥证书」 | ||||||
|  | 6. 下载「支付宝根证书」和「支付宝公钥证书」 | ||||||
|  | 7. 将下载的证书放入本目录,并按上述名称重命名 | ||||||
|  |  | ||||||
|  | ## 配置验证 | ||||||
|  |  | ||||||
|  | 证书配置完成后,系统将使用证书模式进行支付宝API调用,支持V3版本的接口。 | ||||||
							
								
								
									
										1
									
								
								certs/alipay-private-key.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								certs/alipay-private-key.pem
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCV9dEIPimkwxhJXXy2cntNZqeZdV4JAdx7ZpnvbjV6OjeEZczksKFa6lgsYR6Wq6G/khEdVRyNW4RsepBRwVGDBxxUN0nB9uTtPti220PtXZIJycLY585nL/iaUjUt+Cyh2qsyIGQ0lGx9VFWU+x5stGCLgfZbHOaMo0S+49upEsX5p4MyRbc4zA/vf+xgmGm5f1YB1Nwm32JBhVRourmHhi82MLX3Hxwo6R/SIOrDIcUrELYC9XRC2LUWYJIsQ1QMtrE/0XVFmIB68gU9JqRTNXhLCyL19K3WUGvRXG9N5mTaZSBIYco7ZrtbTFHtIxSOYxqYTOtkPKVYxGn1I2WHAgMBAAECggEAEeaA6Dn7YJaFPKSzMVgjDc82LGRNMEgPmI9byq/eJFP0spIwThAjgqW8lreVHikoqqR19IlnWhxVh1luBsRLxZdAs3DSFhwxoXxCBDnSNvBXcWGrJ5csFTcttsYfYPhh44QlsVsaewhIlwFNIfaD4Df72ktOK/wcLSeEGkE6xEixfMcLawg6a8rkHbvJtppFdTSL2YBB4X+dnt3cYCxFYch7Q5+RF+Wqr++860B5ztBRSX8xmqEIDS/95tf09qDyhyrnE2a2wh/jT+BWzvILKNc8goNo29kACo8bE9p+rWCnG8YsL+4emlrxk6/OYzR5lG6xZNvXAmnY0LDKzQsqeQKBgQDTm/uQP82o0ez9YHP/sAvTVq7pIJlEnaZqI2jBvyGxf6fDTU5b0yfJF9VO5EoPVT9CHRJ7A0T4RUDhIAzo9fjrz3VjkXO/RZyOIHsptjvOFeKCiFGrHO8sKxC5i4IZIRwIB7A84DU7rVzQ5Kh3XRqi8srKl/DBXo1BloWU0HJetQKBgQC1axqVH/eIwZrYjSbXgSHwGYMhH0YmOIJEYavD6TmOp9QL5N1ax6B0r/BIVt7XAIRck8kueKY4LeRulq3azXIhBy5Qx6sp2Sb/SX3vrPrvA/ImoDQKR4CeL5zXQJkqbVgbbS//UTEWilKyQXNz13D4qbCqNBbLJaDirXqBT0ycywKBgCMvT2/XvAlzBlXHAOKl0gGM6z5mJjXrhK0nQBbfAeoykKF/rCTGgloEdXpNqSbNhNwoW1dK3t/tG/GS07K0m3QSJbGtkLJgD7zuF6yC2YTVzLjpk7LA99+/NWO0l6g4AiIvrRUiLpfCpqkxK/XU7EXl2uQ+yVBNuW0LayCoXCv1AoGAU/CJbSRMUO9baQTuStoJzODRBltFBtwwkdkrM0tPAU1v1E0BikZBXJwnLiFbm9k2ZOtQM3tJVUcOoYiASnOyccuzx1aLQKKj44yqg2Hi/QIzYWHQkk0BGq/m/sV52OKc2JvNkHGNp+M6XhXgiGHPeI5zGl1dioMPjLI9s2Twir8CgYEAn5wAbpx2imCPDtKgMxb0zxbKB5kq37QGuIRRAoorPkFr0Z7xSZs1M0qkhZcaX/kfnNgigPVwpUeUDQmETBRaXrCuXxRkOXGo1st4v5gn9kgQYBCv0dVbV1QNnnXp2fAtQ/3Jw6l9w4T0PgPBhu9TPnSTVZA0YldNo7TEEL5kEbA= | ||||||
							
								
								
									
										9
									
								
								certs/alipay-public-key.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								certs/alipay-public-key.pem
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | -----BEGIN PUBLIC KEY----- | ||||||
|  | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlfXRCD4ppMMYSV18tnJ7 | ||||||
|  | TWanmXVeCQHce2aZ7241ejo3hGXM5LChWupYLGEelquhv5IRHVUcjVuEbHqQUcFR | ||||||
|  | gwccVDdJwfbk7T7YtttD7V2SCcnC2OfOZy/4mlI1LfgsodqrMiBkNJRsfVRVlPse | ||||||
|  | bLRgi4H2WxzmjKNEvuPbqRLF+aeDMkW3OMwP73/sYJhpuX9WAdTcJt9iQYVUaLq5 | ||||||
|  | h4YvNjC19x8cKOkf0iDqwyHFKxC2AvV0Qti1FmCSLENUDLaxP9F1RZiAevIFPSak | ||||||
|  | UzV4Swsi9fSt1lBr0VxvTeZk2mUgSGHKO2a7W0xR7SMUjmMamEzrZDylWMRp9SNl | ||||||
|  | hwIDAQAB | ||||||
|  | -----END PUBLIC KEY----- | ||||||
							
								
								
									
										3
									
								
								certs/alipayCertPublicKey_RSA2.crt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								certs/alipayCertPublicKey_RSA2.crt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | -----BEGIN PUBLIC KEY----- | ||||||
|  | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5swLKPSzOMucRC52c9kKJZI9cYWDFd+s3UuE+aDtWodGrGV8g3szmp7hUWlaWY/didKc9vQNq93y67eEyw6QsMn26WwlzDbgP0xTcHEt+qDCeAltSqf6MX3KPmlz0f/DNneR9DR9ZGwaW1ATY3kg8gj+kIWngrqgjOv37UJWEpQOxUfWDGTBC1zzhC0PTXY7lX3GUZmDEtDtBs1BsFUdk995TbTD1cTiyDFuea49br0dovmU1ROOg6vK3G9xDd4Mke/opDunLTHe63+fBCnB7FyZ9F8zWg4LYND1QPmIX2m5gwICBHhNm8WqIfp9T64vpAxlM74BEsMlv3hNy0INQQIDAQAB | ||||||
|  | -----END PUBLIC KEY----- | ||||||
							
								
								
									
										3
									
								
								certs/alipayPublicKey_RSA2.crt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								certs/alipayPublicKey_RSA2.crt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | -----BEGIN PUBLIC KEY----- | ||||||
|  | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5swLKPSzOMucRC52c9kKJZI9cYWDFd+s3UuE+aDtWodGrGV8g3szmp7hUWlaWY/didKc9vQNq93y67eEyw6QsMn26WwlzDbgP0xTcHEt+qDCeAltSqf6MX3KPmlz0f/DNneR9DR9ZGwaW1ATY3kg8gj+kIWngrqgjOv37UJWEpQOxUfWDGTBC1zzhC0PTXY7lX3GUZmDEtDtBs1BsFUdk995TbTD1cTiyDFuea49br0dovmU1ROOg6vK3G9xDd4Mke/opDunLTHe63+fBCnB7FyZ9F8zWg4LYND1QPmIX2m5gwICBHhNm8WqIfp9T64vpAxlM74BEsMlv3hNy0INQQIDAQAB | ||||||
|  | -----END PUBLIC KEY----- | ||||||
							
								
								
									
										3
									
								
								certs/appCertPublicKey.crt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								certs/appCertPublicKey.crt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | -----BEGIN PUBLIC KEY----- | ||||||
|  | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlfXRCD4ppMMYSV18tnJ7TWanmXVeCQHce2aZ7241ejo3hGXM5LChWupYLGEelquhv5IRHVUcjVuEbHqQUcFRgwccVDdJwfbk7T7YtttD7V2SCcnC2OfOZy/4mlI1LfgsodqrMiBkNJRsfVRVlPsebLRgi4H2WxzmjKNEvuPbqRLF+aeDMkW3OMwP73/sYJhpuX9WAdTcJt9iQYVUaLq5h4YvNjC19x8cKOkf0iDqwyHFKxC2AvV0Qti1FmCSLENUDLaxP9F1RZiAevIFPSakUzV4Swsi9fSt1lBr0VxvTeZk2mUgSGHKO2a7W0xR7SMUjmMamEzrZDylWMRp9SNlhwIDAQAB | ||||||
|  | -----END PUBLIC KEY----- | ||||||
| @@ -574,7 +574,48 @@ async function createTables() { | |||||||
|       FOREIGN KEY (order_id) REFERENCES matching_orders(id) ON DELETE SET NULL |       FOREIGN KEY (order_id) REFERENCES matching_orders(id) ON DELETE SET NULL | ||||||
|     ) |     ) | ||||||
|   `); |   `); | ||||||
| } |  | ||||||
|  |   // 通知公告表 | ||||||
|  |   await getDB().execute(` | ||||||
|  |     CREATE TABLE IF NOT EXISTS announcements ( | ||||||
|  |       id INT AUTO_INCREMENT PRIMARY KEY, | ||||||
|  |       title VARCHAR(255) NOT NULL COMMENT '公告标题', | ||||||
|  |       content TEXT NOT NULL COMMENT '公告内容', | ||||||
|  |       type ENUM('system', 'maintenance', 'promotion', 'warning') DEFAULT 'system' COMMENT '公告类型', | ||||||
|  |       priority ENUM('low', 'medium', 'high', 'urgent') DEFAULT 'medium' COMMENT '优先级', | ||||||
|  |       status ENUM('draft', 'published', 'archived') DEFAULT 'draft' COMMENT '状态', | ||||||
|  |       is_pinned BOOLEAN DEFAULT FALSE COMMENT '是否置顶', | ||||||
|  |       publish_time TIMESTAMP NULL COMMENT '发布时间', | ||||||
|  |       expire_time TIMESTAMP NULL COMMENT '过期时间', | ||||||
|  |       created_by INT NOT NULL COMMENT '创建者ID', | ||||||
|  |       created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||||||
|  |       updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', | ||||||
|  |       FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE, | ||||||
|  |       INDEX idx_status (status), | ||||||
|  |       INDEX idx_type (type), | ||||||
|  |       INDEX idx_publish_time (publish_time), | ||||||
|  |       INDEX idx_created_at (created_at) | ||||||
|  |     ) COMMENT='通知公告表' | ||||||
|  |   `); | ||||||
|  |  | ||||||
|  |   // 用户公告阅读状态表 | ||||||
|  |   await getDB().execute(` | ||||||
|  |     CREATE TABLE IF NOT EXISTS user_announcement_reads ( | ||||||
|  |       id INT AUTO_INCREMENT PRIMARY KEY, | ||||||
|  |       user_id INT NOT NULL COMMENT '用户ID', | ||||||
|  |       announcement_id INT NOT NULL COMMENT '公告ID', | ||||||
|  |       is_read BOOLEAN DEFAULT FALSE COMMENT '是否已读', | ||||||
|  |       read_at TIMESTAMP NULL COMMENT '阅读时间', | ||||||
|  |       created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||||||
|  |       updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', | ||||||
|  |       UNIQUE KEY unique_user_announcement (user_id, announcement_id), | ||||||
|  |       FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, | ||||||
|  |       FOREIGN KEY (announcement_id) REFERENCES announcements(id) ON DELETE CASCADE, | ||||||
|  |       INDEX idx_user_id (user_id), | ||||||
|  |       INDEX idx_announcement_id (announcement_id), | ||||||
|  |       INDEX idx_is_read (is_read) | ||||||
|  |     ) COMMENT='用户公告阅读状态表' | ||||||
|  |   `);} | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 添加缺失的字段(处理数据库升级) |  * 添加缺失的字段(处理数据库升级) | ||||||
| @@ -862,13 +903,13 @@ async function createDefaultData() { | |||||||
|   } |   } | ||||||
|    |    | ||||||
|   // 初始化浙江省区域数据 |   // 初始化浙江省区域数据 | ||||||
|   await initializeZhejiangRegions(); |   // await initializeZhejiangRegions(); | ||||||
|    |    | ||||||
|   // 初始化默认地址标签 |   // 初始化默认地址标签 | ||||||
|   await initializeDefaultAddressLabels(); |   // await initializeDefaultAddressLabels(); | ||||||
|    |    | ||||||
|   // 初始化全国省市区数据 |   // 初始化全国省市区数据 | ||||||
|   await initializeChinaRegions(); |   // await initializeChinaRegions(); | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|   | |||||||
							
								
								
									
										24
									
								
								config/wechatPay.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								config/wechatPay.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | // 微信支付配置 | ||||||
|  | module.exports = { | ||||||
|  |   // 微信支付配置 | ||||||
|  |   wechatPay: { | ||||||
|  |     appId: process.env.WECHAT_APP_ID || '', // 微信公众号AppID | ||||||
|  |     mchId: process.env.WECHAT_MCH_ID || '', // 商户号 | ||||||
|  |     apiKey: process.env.WECHAT_API_KEY || '', // API密钥 | ||||||
|  |     apiV3Key: process.env.WECHAT_API_V3_KEY || '', // APIv3密钥 | ||||||
|  |     notifyUrl: process.env.WECHAT_NOTIFY_URL || 'https://your-domain.com/api/wechat/notify', // 支付回调地址 | ||||||
|  |      | ||||||
|  |     // 证书路径(生产环境需要配置) | ||||||
|  |     certPath: process.env.WECHAT_CERT_PATH || '', | ||||||
|  |     keyPath: process.env.WECHAT_KEY_PATH || '', | ||||||
|  |      | ||||||
|  |     // 支付相关配置 | ||||||
|  |     tradeType: { | ||||||
|  |       h5: 'MWEB', // H5支付 | ||||||
|  |       jsapi: 'JSAPI' // 公众号支付 | ||||||
|  |     }, | ||||||
|  |      | ||||||
|  |     // 注册费用配置(单位:分) | ||||||
|  |     registrationFee: 100 // 1元注册费 | ||||||
|  |   } | ||||||
|  | }; | ||||||
| @@ -11,10 +11,12 @@ docs/ | |||||||
| │   ├── product.js     # 商品相关数据模型 | │   ├── product.js     # 商品相关数据模型 | ||||||
| │   ├── order.js       # 订单相关数据模型 | │   ├── order.js       # 订单相关数据模型 | ||||||
| │   ├── user.js        # 用户相关数据模型 | │   ├── user.js        # 用户相关数据模型 | ||||||
| │   └── cart.js        # 购物车相关数据模型 | │   ├── cart.js        # 购物车相关数据模型 | ||||||
|  | │   └── announcement.js # 通知公告相关数据模型 | ||||||
| └── apis/              # API 接口定义 | └── apis/              # API 接口定义 | ||||||
|     ├── products.js    # 商品相关 API |     ├── products.js    # 商品相关 API | ||||||
|     └── orders.js      # 订单相关 API |     ├── orders.js      # 订单相关 API | ||||||
|  |     └── announcements.js # 通知公告相关 API | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ## 优势 | ## 优势 | ||||||
|   | |||||||
							
								
								
									
										736
									
								
								docs/apis/announcements.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										736
									
								
								docs/apis/announcements.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,736 @@ | |||||||
|  | /** | ||||||
|  |  * @swagger | ||||||
|  |  * tags: | ||||||
|  |  *   name: Announcements | ||||||
|  |  *   description: 通知公告管理API | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @swagger | ||||||
|  |  * /api/announcements/{id}: | ||||||
|  |  *   get: | ||||||
|  |  *     summary: 获取单个公告详情 | ||||||
|  |  *     tags: [Announcements] | ||||||
|  |  *     security: | ||||||
|  |  *       - bearerAuth: [] | ||||||
|  |  *     parameters: | ||||||
|  |  *       - in: path | ||||||
|  |  *         name: id | ||||||
|  |  *         required: true | ||||||
|  |  *         schema: | ||||||
|  |  *           type: integer | ||||||
|  |  *         description: 公告ID | ||||||
|  |  *     responses: | ||||||
|  |  *       200: | ||||||
|  |  *         description: 成功获取公告详情 | ||||||
|  |  *         content: | ||||||
|  |  *           application/json: | ||||||
|  |  *             schema: | ||||||
|  |  *               type: object | ||||||
|  |  *               properties: | ||||||
|  |  *                 success: | ||||||
|  |  *                   type: boolean | ||||||
|  |  *                 data: | ||||||
|  |  *                   $ref: '#/components/schemas/Announcement' | ||||||
|  |  *       401: | ||||||
|  |  *         description: 未授权 | ||||||
|  |  *       404: | ||||||
|  |  *         description: 公告不存在 | ||||||
|  |  *       500: | ||||||
|  |  *         description: 服务器错误 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @swagger | ||||||
|  |  * /api/announcements/{id}/read: | ||||||
|  |  *   post: | ||||||
|  |  *     summary: 标记公告为已读 | ||||||
|  |  *     tags: [Announcements] | ||||||
|  |  *     security: | ||||||
|  |  *       - bearerAuth: [] | ||||||
|  |  *     parameters: | ||||||
|  |  *       - in: path | ||||||
|  |  *         name: id | ||||||
|  |  *         required: true | ||||||
|  |  *         schema: | ||||||
|  |  *           type: integer | ||||||
|  |  *         description: 公告ID | ||||||
|  |  *     responses: | ||||||
|  |  *       200: | ||||||
|  |  *         description: 标记已读成功 | ||||||
|  |  *         content: | ||||||
|  |  *           application/json: | ||||||
|  |  *             schema: | ||||||
|  |  *               type: object | ||||||
|  |  *               properties: | ||||||
|  |  *                 success: | ||||||
|  |  *                   type: boolean | ||||||
|  |  *                   example: true | ||||||
|  |  *                 message: | ||||||
|  |  *                   type: string | ||||||
|  |  *                   example: "已标记为已读" | ||||||
|  |  *       401: | ||||||
|  |  *         description: 未授权 | ||||||
|  |  *       404: | ||||||
|  |  *         description: 公告不存在 | ||||||
|  |  *       500: | ||||||
|  |  *         description: 服务器错误 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @swagger | ||||||
|  |  * /api/announcements/unread/count: | ||||||
|  |  *   get: | ||||||
|  |  *     summary: 获取用户未读公告数量 | ||||||
|  |  *     tags: [Announcements] | ||||||
|  |  *     security: | ||||||
|  |  *       - bearerAuth: [] | ||||||
|  |  *     responses: | ||||||
|  |  *       200: | ||||||
|  |  *         description: 获取未读数量成功 | ||||||
|  |  *         content: | ||||||
|  |  *           application/json: | ||||||
|  |  *             schema: | ||||||
|  |  *               type: object | ||||||
|  |  *               properties: | ||||||
|  |  *                 success: | ||||||
|  |  *                   type: boolean | ||||||
|  |  *                   example: true | ||||||
|  |  *                 data: | ||||||
|  |  *                   type: object | ||||||
|  |  *                   properties: | ||||||
|  |  *                     unread_count: | ||||||
|  |  *                       type: integer | ||||||
|  |  *                       example: 5 | ||||||
|  |  *                       description: 未读公告数量 | ||||||
|  |  *       401: | ||||||
|  |  *         description: 未授权 | ||||||
|  |  *       500: | ||||||
|  |  *         description: 服务器错误 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @swagger | ||||||
|  |  * /api/announcements/batch/read: | ||||||
|  |  *   post: | ||||||
|  |  *     summary: 批量标记公告为已读 | ||||||
|  |  *     tags: [Announcements] | ||||||
|  |  *     security: | ||||||
|  |  *       - bearerAuth: [] | ||||||
|  |  *     requestBody: | ||||||
|  |  *       required: true | ||||||
|  |  *       content: | ||||||
|  |  *         application/json: | ||||||
|  |  *           schema: | ||||||
|  |  *             type: object | ||||||
|  |  *             required: | ||||||
|  |  *               - announcement_ids | ||||||
|  |  *             properties: | ||||||
|  |  *               announcement_ids: | ||||||
|  |  *                 type: array | ||||||
|  |  *                 items: | ||||||
|  |  *                   type: integer | ||||||
|  |  *                 example: [1, 2, 3] | ||||||
|  |  *                 description: 公告ID列表 | ||||||
|  |  *     responses: | ||||||
|  |  *       200: | ||||||
|  |  *         description: 批量标记已读成功 | ||||||
|  |  *         content: | ||||||
|  |  *           application/json: | ||||||
|  |  *             schema: | ||||||
|  |  *               type: object | ||||||
|  |  *               properties: | ||||||
|  |  *                 success: | ||||||
|  |  *                   type: boolean | ||||||
|  |  *                   example: true | ||||||
|  |  *                 message: | ||||||
|  |  *                   type: string | ||||||
|  |  *                   example: "批量标记已读成功" | ||||||
|  |  *       400: | ||||||
|  |  *         description: 请求参数错误 | ||||||
|  |  *       401: | ||||||
|  |  *         description: 未授权 | ||||||
|  |  *       500: | ||||||
|  |  *         description: 服务器错误 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @swagger | ||||||
|  |  * /api/announcements: | ||||||
|  |  *   post: | ||||||
|  |  *     summary: 创建新公告 | ||||||
|  |  *     tags: [Announcements] | ||||||
|  |  *     security: | ||||||
|  |  *       - bearerAuth: [] | ||||||
|  |  *     requestBody: | ||||||
|  |  *       required: true | ||||||
|  |  *       content: | ||||||
|  |  *         application/json: | ||||||
|  |  *           schema: | ||||||
|  |  *             type: object | ||||||
|  |  *             required: | ||||||
|  |  *               - title | ||||||
|  |  *               - content | ||||||
|  |  *             properties: | ||||||
|  |  *               title: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 description: 公告标题 | ||||||
|  |  *               content: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 description: 公告内容 | ||||||
|  |  *               type: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 enum: [system, maintenance, promotion, warning] | ||||||
|  |  *                 default: system | ||||||
|  |  *               priority: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 enum: [low, medium, high, urgent] | ||||||
|  |  *                 default: medium | ||||||
|  |  *               status: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 enum: [draft, published] | ||||||
|  |  *                 default: draft | ||||||
|  |  *               is_pinned: | ||||||
|  |  *                 type: boolean | ||||||
|  |  *                 default: false | ||||||
|  |  *               publish_time: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 format: date-time | ||||||
|  |  *               expire_time: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 format: date-time | ||||||
|  |  *     responses: | ||||||
|  |  *       201: | ||||||
|  |  *         description: 公告创建成功 | ||||||
|  |  *         content: | ||||||
|  |  *           application/json: | ||||||
|  |  *             schema: | ||||||
|  |  *               type: object | ||||||
|  |  *               properties: | ||||||
|  |  *                 success: | ||||||
|  |  *                   type: boolean | ||||||
|  |  *                 message: | ||||||
|  |  *                   type: string | ||||||
|  |  *                 data: | ||||||
|  |  *                   type: object | ||||||
|  |  *                   properties: | ||||||
|  |  *                     id: | ||||||
|  |  *                       type: integer | ||||||
|  |  *       400: | ||||||
|  |  *         description: 请求参数错误 | ||||||
|  |  *       401: | ||||||
|  |  *         description: 未授权 | ||||||
|  |  *       500: | ||||||
|  |  *         description: 服务器错误 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @swagger | ||||||
|  |  * /api/announcements/{id}: | ||||||
|  |  *   put: | ||||||
|  |  *     summary: 更新公告 | ||||||
|  |  *     tags: [Announcements] | ||||||
|  |  *     security: | ||||||
|  |  *       - bearerAuth: [] | ||||||
|  |  *     parameters: | ||||||
|  |  *       - in: path | ||||||
|  |  *         name: id | ||||||
|  |  *         required: true | ||||||
|  |  *         schema: | ||||||
|  |  *           type: integer | ||||||
|  |  *         description: 公告ID | ||||||
|  |  *     requestBody: | ||||||
|  |  *       required: true | ||||||
|  |  *       content: | ||||||
|  |  *         application/json: | ||||||
|  |  *           schema: | ||||||
|  |  *             type: object | ||||||
|  |  *             properties: | ||||||
|  |  *               title: | ||||||
|  |  *                 type: string | ||||||
|  |  *               content: | ||||||
|  |  *                 type: string | ||||||
|  |  *               type: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 enum: [system, maintenance, promotion, warning] | ||||||
|  |  *               priority: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 enum: [low, medium, high, urgent] | ||||||
|  |  *               status: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 enum: [draft, published, archived] | ||||||
|  |  *               is_pinned: | ||||||
|  |  *                 type: boolean | ||||||
|  |  *               publish_time: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 format: date-time | ||||||
|  |  *               expire_time: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 format: date-time | ||||||
|  |  *     responses: | ||||||
|  |  *       200: | ||||||
|  |  *         description: 公告更新成功 | ||||||
|  |  *         content: | ||||||
|  |  *           application/json: | ||||||
|  |  *             schema: | ||||||
|  |  *               type: object | ||||||
|  |  *               properties: | ||||||
|  |  *                 success: | ||||||
|  |  *                   type: boolean | ||||||
|  |  *                 message: | ||||||
|  |  *                   type: string | ||||||
|  |  *                 data: | ||||||
|  |  *                   $ref: '#/components/schemas/Announcement' | ||||||
|  |  *       400: | ||||||
|  |  *         description: 请求参数错误 | ||||||
|  |  *       401: | ||||||
|  |  *         description: 未授权 | ||||||
|  |  *       404: | ||||||
|  |  *         description: 公告不存在 | ||||||
|  |  *       500: | ||||||
|  |  *         description: 服务器错误 | ||||||
|  |  * | ||||||
|  |  *   delete: | ||||||
|  |  *     summary: 删除公告 | ||||||
|  |  *     tags: [Announcements] | ||||||
|  |  *     security: | ||||||
|  |  *       - bearerAuth: [] | ||||||
|  |  *     parameters: | ||||||
|  |  *       - in: path | ||||||
|  |  *         name: id | ||||||
|  |  *         required: true | ||||||
|  |  *         schema: | ||||||
|  |  *           type: integer | ||||||
|  |  *         description: 公告ID | ||||||
|  |  *     responses: | ||||||
|  |  *       200: | ||||||
|  |  *         description: 公告删除成功 | ||||||
|  |  *         content: | ||||||
|  |  *           application/json: | ||||||
|  |  *             schema: | ||||||
|  |  *               type: object | ||||||
|  |  *               properties: | ||||||
|  |  *                 success: | ||||||
|  |  *                   type: boolean | ||||||
|  |  *                 message: | ||||||
|  |  *                   type: string | ||||||
|  |  *       401: | ||||||
|  |  *         description: 未授权 | ||||||
|  |  *       404: | ||||||
|  |  *         description: 公告不存在 | ||||||
|  |  *       500: | ||||||
|  |  *         description: 服务器错误 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @swagger | ||||||
|  |  * /api/announcements/public/list: | ||||||
|  |  *   get: | ||||||
|  |  *     summary: 获取公开发布的公告列表(无需认证) | ||||||
|  |  *     tags: [Announcements] | ||||||
|  |  *     parameters: | ||||||
|  |  *       - in: query | ||||||
|  |  *         name: limit | ||||||
|  |  *         schema: | ||||||
|  |  *           type: integer | ||||||
|  |  *           default: 5 | ||||||
|  |  *         description: 获取数量 | ||||||
|  |  *     responses: | ||||||
|  |  *       200: | ||||||
|  |  *         description: 成功获取公开公告列表 | ||||||
|  |  *         content: | ||||||
|  |  *           application/json: | ||||||
|  |  *             schema: | ||||||
|  |  *               type: object | ||||||
|  |  *               properties: | ||||||
|  |  *                 success: | ||||||
|  |  *                   type: boolean | ||||||
|  |  *                 data: | ||||||
|  |  *                   type: array | ||||||
|  |  *                   items: | ||||||
|  |  *                     $ref: '#/components/schemas/Announcement' | ||||||
|  |  *       500: | ||||||
|  |  *         description: 服务器错误 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @swagger | ||||||
|  |  * components: | ||||||
|  |  *   schemas: | ||||||
|  |  *     Announcement: | ||||||
|  |  *       type: object | ||||||
|  |  *       required: | ||||||
|  |  *         - title | ||||||
|  |  *         - content | ||||||
|  |  *       properties: | ||||||
|  |  *         id: | ||||||
|  |  *           type: integer | ||||||
|  |  *           description: 公告ID | ||||||
|  |  *         title: | ||||||
|  |  *           type: string | ||||||
|  |  *           description: 公告标题 | ||||||
|  |  *         content: | ||||||
|  |  *           type: string | ||||||
|  |  *           description: 公告内容 | ||||||
|  |  *         type: | ||||||
|  |  *           type: string | ||||||
|  |  *           description: 公告类型 | ||||||
|  |  *           enum: [system, maintenance, promotion, warning] | ||||||
|  |  *         priority: | ||||||
|  |  *           type: string | ||||||
|  |  *           description: 优先级 | ||||||
|  |  *           enum: [low, medium, high, urgent] | ||||||
|  |  *         status: | ||||||
|  |  *           type: string | ||||||
|  |  *           description: 状态 | ||||||
|  |  *           enum: [draft, published, archived] | ||||||
|  |  *         is_pinned: | ||||||
|  |  *           type: boolean | ||||||
|  |  *           description: 是否置顶 | ||||||
|  |  *         publish_time: | ||||||
|  |  *           type: string | ||||||
|  |  *           format: date-time | ||||||
|  |  *           description: 发布时间 | ||||||
|  |  *         expire_time: | ||||||
|  |  *           type: string | ||||||
|  |  *           format: date-time | ||||||
|  |  *           description: 过期时间 | ||||||
|  |  *         created_by: | ||||||
|  |  *           type: integer | ||||||
|  |  *           description: 创建者ID | ||||||
|  |  *         created_at: | ||||||
|  |  *           type: string | ||||||
|  |  *           format: date-time | ||||||
|  |  *           description: 创建时间 | ||||||
|  |  *         updated_at: | ||||||
|  |  *           type: string | ||||||
|  |  *           format: date-time | ||||||
|  |  *           description: 更新时间 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @swagger | ||||||
|  |  * /api/announcements: | ||||||
|  |  *   get: | ||||||
|  |  *     summary: 获取通知公告列表 | ||||||
|  |  *     tags: [Announcements] | ||||||
|  |  *     security: | ||||||
|  |  *       - bearerAuth: [] | ||||||
|  |  *     parameters: | ||||||
|  |  *       - in: query | ||||||
|  |  *         name: page | ||||||
|  |  *         schema: | ||||||
|  |  *           type: integer | ||||||
|  |  *           default: 1 | ||||||
|  |  *         description: 页码 | ||||||
|  |  *       - in: query | ||||||
|  |  *         name: limit | ||||||
|  |  *         schema: | ||||||
|  |  *           type: integer | ||||||
|  |  *           default: 10 | ||||||
|  |  *         description: 每页数量 | ||||||
|  |  *       - in: query | ||||||
|  |  *         name: search | ||||||
|  |  *         schema: | ||||||
|  |  *           type: string | ||||||
|  |  *         description: 搜索关键词(标题或内容) | ||||||
|  |  *       - in: query | ||||||
|  |  *         name: type | ||||||
|  |  *         schema: | ||||||
|  |  *           type: string | ||||||
|  |  *           enum: [system, activity, maintenance, urgent] | ||||||
|  |  *         description: 公告类型 | ||||||
|  |  *       - in: query | ||||||
|  |  *         name: priority | ||||||
|  |  *         schema: | ||||||
|  |  *           type: string | ||||||
|  |  *           enum: [high, medium, low] | ||||||
|  |  *         description: 优先级 | ||||||
|  |  *       - in: query | ||||||
|  |  *         name: status | ||||||
|  |  *         schema: | ||||||
|  |  *           type: string | ||||||
|  |  *           enum: [draft, published, expired] | ||||||
|  |  *         description: 状态 | ||||||
|  |  *       - in: query | ||||||
|  |  *         name: isTop | ||||||
|  |  *         schema: | ||||||
|  |  *           type: boolean | ||||||
|  |  *         description: 是否置顶 | ||||||
|  |  *       - in: query | ||||||
|  |  *         name: sortBy | ||||||
|  |  *         schema: | ||||||
|  |  *           type: string | ||||||
|  |  *           enum: [created_at, updated_at, publish_time, priority] | ||||||
|  |  *           default: created_at | ||||||
|  |  *         description: 排序字段 | ||||||
|  |  *       - in: query | ||||||
|  |  *         name: sortOrder | ||||||
|  |  *         schema: | ||||||
|  |  *           type: string | ||||||
|  |  *           enum: [ASC, DESC] | ||||||
|  |  *           default: DESC | ||||||
|  |  *         description: 排序方向 | ||||||
|  |  *     responses: | ||||||
|  |  *       200: | ||||||
|  |  *         description: 成功获取公告列表 | ||||||
|  |  *         content: | ||||||
|  |  *           application/json: | ||||||
|  |  *             schema: | ||||||
|  |  *               type: object | ||||||
|  |  *               properties: | ||||||
|  |  *                 success: | ||||||
|  |  *                   type: boolean | ||||||
|  |  *                   example: true | ||||||
|  |  *                 data: | ||||||
|  |  *                   type: object | ||||||
|  |  *                   properties: | ||||||
|  |  *                     announcements: | ||||||
|  |  *                       type: array | ||||||
|  |  *                       items: | ||||||
|  |  *                         $ref: '#/components/schemas/Announcement' | ||||||
|  |  *                     total: | ||||||
|  |  *                       type: integer | ||||||
|  |  *                       example: 50 | ||||||
|  |  *                     page: | ||||||
|  |  *                       type: integer | ||||||
|  |  *                       example: 1 | ||||||
|  |  *                     limit: | ||||||
|  |  *                       type: integer | ||||||
|  |  *                       example: 10 | ||||||
|  |  *                     totalPages: | ||||||
|  |  *                       type: integer | ||||||
|  |  *                       example: 5 | ||||||
|  |  *       401: | ||||||
|  |  *         description: 未授权 | ||||||
|  |  *       500: | ||||||
|  |  *         description: 服务器错误 | ||||||
|  |  * | ||||||
|  |  *   post: | ||||||
|  |  *     summary: 创建新的通知公告 | ||||||
|  |  *     tags: [Announcements] | ||||||
|  |  *     security: | ||||||
|  |  *       - bearerAuth: [] | ||||||
|  |  *     requestBody: | ||||||
|  |  *       required: true | ||||||
|  |  *       content: | ||||||
|  |  *         application/json: | ||||||
|  |  *           schema: | ||||||
|  |  *             type: object | ||||||
|  |  *             required: | ||||||
|  |  *               - title | ||||||
|  |  *               - content | ||||||
|  |  *               - type | ||||||
|  |  *               - priority | ||||||
|  |  *             properties: | ||||||
|  |  *               title: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 description: 公告标题 | ||||||
|  |  *                 example: "系统维护通知" | ||||||
|  |  *               content: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 description: 公告内容 | ||||||
|  |  *                 example: "系统将于今晚进行维护,预计维护时间2小时" | ||||||
|  |  *               type: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 enum: [system, activity, maintenance, urgent] | ||||||
|  |  *                 description: 公告类型 | ||||||
|  |  *                 example: "maintenance" | ||||||
|  |  *               priority: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 enum: [high, medium, low] | ||||||
|  |  *                 description: 优先级 | ||||||
|  |  *                 example: "high" | ||||||
|  |  *               status: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 enum: [draft, published] | ||||||
|  |  *                 default: draft | ||||||
|  |  *                 description: 状态 | ||||||
|  |  *               isTop: | ||||||
|  |  *                 type: boolean | ||||||
|  |  *                 default: false | ||||||
|  |  *                 description: 是否置顶 | ||||||
|  |  *               publishTime: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 format: date-time | ||||||
|  |  *                 description: 发布时间 | ||||||
|  |  *               expireTime: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 format: date-time | ||||||
|  |  *                 description: 过期时间 | ||||||
|  |  *     responses: | ||||||
|  |  *       201: | ||||||
|  |  *         description: 公告创建成功 | ||||||
|  |  *         content: | ||||||
|  |  *           application/json: | ||||||
|  |  *             schema: | ||||||
|  |  *               type: object | ||||||
|  |  *               properties: | ||||||
|  |  *                 success: | ||||||
|  |  *                   type: boolean | ||||||
|  |  *                   example: true | ||||||
|  |  *                 message: | ||||||
|  |  *                   type: string | ||||||
|  |  *                   example: "公告创建成功" | ||||||
|  |  *                 data: | ||||||
|  |  *                   $ref: '#/components/schemas/Announcement' | ||||||
|  |  *       400: | ||||||
|  |  *         description: 请求参数错误 | ||||||
|  |  *       401: | ||||||
|  |  *         description: 未授权 | ||||||
|  |  *       500: | ||||||
|  |  *         description: 服务器错误 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @swagger | ||||||
|  |  * /api/announcements/{id}: | ||||||
|  |  *   put: | ||||||
|  |  *     summary: 更新通知公告 | ||||||
|  |  *     tags: [Announcements] | ||||||
|  |  *     security: | ||||||
|  |  *       - bearerAuth: [] | ||||||
|  |  *     parameters: | ||||||
|  |  *       - in: path | ||||||
|  |  *         name: id | ||||||
|  |  *         required: true | ||||||
|  |  *         schema: | ||||||
|  |  *           type: integer | ||||||
|  |  *         description: 公告ID | ||||||
|  |  *     requestBody: | ||||||
|  |  *       required: true | ||||||
|  |  *       content: | ||||||
|  |  *         application/json: | ||||||
|  |  *           schema: | ||||||
|  |  *             type: object | ||||||
|  |  *             properties: | ||||||
|  |  *               title: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 description: 公告标题 | ||||||
|  |  *               content: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 description: 公告内容 | ||||||
|  |  *               type: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 enum: [system, activity, maintenance, urgent] | ||||||
|  |  *                 description: 公告类型 | ||||||
|  |  *               priority: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 enum: [high, medium, low] | ||||||
|  |  *                 description: 优先级 | ||||||
|  |  *               status: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 enum: [draft, published, expired] | ||||||
|  |  *                 description: 状态 | ||||||
|  |  *               isTop: | ||||||
|  |  *                 type: boolean | ||||||
|  |  *                 description: 是否置顶 | ||||||
|  |  *               publishTime: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 format: date-time | ||||||
|  |  *                 description: 发布时间 | ||||||
|  |  *               expireTime: | ||||||
|  |  *                 type: string | ||||||
|  |  *                 format: date-time | ||||||
|  |  *                 description: 过期时间 | ||||||
|  |  *     responses: | ||||||
|  |  *       200: | ||||||
|  |  *         description: 公告更新成功 | ||||||
|  |  *         content: | ||||||
|  |  *           application/json: | ||||||
|  |  *             schema: | ||||||
|  |  *               type: object | ||||||
|  |  *               properties: | ||||||
|  |  *                 success: | ||||||
|  |  *                   type: boolean | ||||||
|  |  *                   example: true | ||||||
|  |  *                 message: | ||||||
|  |  *                   type: string | ||||||
|  |  *                   example: "公告更新成功" | ||||||
|  |  *                 data: | ||||||
|  |  *                   $ref: '#/components/schemas/Announcement' | ||||||
|  |  *       400: | ||||||
|  |  *         description: 请求参数错误 | ||||||
|  |  *       401: | ||||||
|  |  *         description: 未授权 | ||||||
|  |  *       404: | ||||||
|  |  *         description: 公告不存在 | ||||||
|  |  *       500: | ||||||
|  |  *         description: 服务器错误 | ||||||
|  |  * | ||||||
|  |  *   delete: | ||||||
|  |  *     summary: 删除通知公告 | ||||||
|  |  *     tags: [Announcements] | ||||||
|  |  *     security: | ||||||
|  |  *       - bearerAuth: [] | ||||||
|  |  *     parameters: | ||||||
|  |  *       - in: path | ||||||
|  |  *         name: id | ||||||
|  |  *         required: true | ||||||
|  |  *         schema: | ||||||
|  |  *           type: integer | ||||||
|  |  *         description: 公告ID | ||||||
|  |  *     responses: | ||||||
|  |  *       200: | ||||||
|  |  *         description: 公告删除成功 | ||||||
|  |  *         content: | ||||||
|  |  *           application/json: | ||||||
|  |  *             schema: | ||||||
|  |  *               type: object | ||||||
|  |  *               properties: | ||||||
|  |  *                 success: | ||||||
|  |  *                   type: boolean | ||||||
|  |  *                   example: true | ||||||
|  |  *                 message: | ||||||
|  |  *                   type: string | ||||||
|  |  *                   example: "公告删除成功" | ||||||
|  |  *       401: | ||||||
|  |  *         description: 未授权 | ||||||
|  |  *       404: | ||||||
|  |  *         description: 公告不存在 | ||||||
|  |  *       500: | ||||||
|  |  *         description: 服务器错误 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @swagger | ||||||
|  |  * /api/announcements/{id}/toggle-top: | ||||||
|  |  *   put: | ||||||
|  |  *     summary: 切换公告置顶状态 | ||||||
|  |  *     tags: [Announcements] | ||||||
|  |  *     security: | ||||||
|  |  *       - bearerAuth: [] | ||||||
|  |  *     parameters: | ||||||
|  |  *       - in: path | ||||||
|  |  *         name: id | ||||||
|  |  *         required: true | ||||||
|  |  *         schema: | ||||||
|  |  *           type: integer | ||||||
|  |  *         description: 公告ID | ||||||
|  |  *     responses: | ||||||
|  |  *       200: | ||||||
|  |  *         description: 置顶状态切换成功 | ||||||
|  |  *         content: | ||||||
|  |  *           application/json: | ||||||
|  |  *             schema: | ||||||
|  |  *               type: object | ||||||
|  |  *               properties: | ||||||
|  |  *                 success: | ||||||
|  |  *                   type: boolean | ||||||
|  |  *                   example: true | ||||||
|  |  *                 message: | ||||||
|  |  *                   type: string | ||||||
|  |  *                   example: "置顶状态更新成功" | ||||||
|  |  *                 data: | ||||||
|  |  *                   type: object | ||||||
|  |  *                   properties: | ||||||
|  |  *                     isTop: | ||||||
|  |  *                       type: boolean | ||||||
|  |  *                       example: true | ||||||
|  |  *       401: | ||||||
|  |  *         description: 未授权 | ||||||
|  |  *       404: | ||||||
|  |  *         description: 公告不存在 | ||||||
|  |  *       500: | ||||||
|  |  *         description: 服务器错误 | ||||||
|  |  */ | ||||||
| @@ -7,149 +7,12 @@ | |||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @swagger |  * @swagger | ||||||
|  * /orders/create-from-cart: |  * /api/orders: | ||||||
|  *   post: |  | ||||||
|  *     summary: 从购物车创建预订单 |  | ||||||
|  *     tags: [Orders] |  | ||||||
|  *     requestBody: |  | ||||||
|  *       required: true |  | ||||||
|  *       content: |  | ||||||
|  *         application/json: |  | ||||||
|  *           schema: |  | ||||||
|  *             type: object |  | ||||||
|  *             required: |  | ||||||
|  *               - cartIds |  | ||||||
|  *             properties: |  | ||||||
|  *               cartIds: |  | ||||||
|  *                 type: array |  | ||||||
|  *                 items: |  | ||||||
|  *                   type: integer |  | ||||||
|  *                 description: 购物车商品ID数组 |  | ||||||
|  *     responses: |  | ||||||
|  *       200: |  | ||||||
|  *         description: 成功创建预订单 |  | ||||||
|  *         content: |  | ||||||
|  *           application/json: |  | ||||||
|  *             schema: |  | ||||||
|  *               type: object |  | ||||||
|  *               properties: |  | ||||||
|  *                 success: |  | ||||||
|  *                   type: boolean |  | ||||||
|  *                 data: |  | ||||||
|  *                   $ref: '#/components/schemas/PreOrder' |  | ||||||
|  *       400: |  | ||||||
|  *         description: 请求参数错误 |  | ||||||
|  *         content: |  | ||||||
|  *           application/json: |  | ||||||
|  *             schema: |  | ||||||
|  *               type: object |  | ||||||
|  *               properties: |  | ||||||
|  *                 success: |  | ||||||
|  *                   type: boolean |  | ||||||
|  *                 message: |  | ||||||
|  *                   type: string |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @swagger |  | ||||||
|  * /orders/pre-order/{id}: |  | ||||||
|  *   get: |  *   get: | ||||||
|  *     summary: 获取预订单详情 |  *     summary: 获取订单列表 | ||||||
|  *     tags: [Orders] |  | ||||||
|  *     parameters: |  | ||||||
|  *       - in: path |  | ||||||
|  *         name: id |  | ||||||
|  *         required: true |  | ||||||
|  *         schema: |  | ||||||
|  *           type: integer |  | ||||||
|  *         description: 预订单ID |  | ||||||
|  *     responses: |  | ||||||
|  *       200: |  | ||||||
|  *         description: 成功获取预订单详情 |  | ||||||
|  *         content: |  | ||||||
|  *           application/json: |  | ||||||
|  *             schema: |  | ||||||
|  *               type: object |  | ||||||
|  *               properties: |  | ||||||
|  *                 success: |  | ||||||
|  *                   type: boolean |  | ||||||
|  *                 data: |  | ||||||
|  *                   type: object |  | ||||||
|  *                   properties: |  | ||||||
|  *                     preOrder: |  | ||||||
|  *                       $ref: '#/components/schemas/PreOrder' |  | ||||||
|  *                     items: |  | ||||||
|  *                       type: array |  | ||||||
|  *                       items: |  | ||||||
|  *                         type: object |  | ||||||
|  *                         properties: |  | ||||||
|  *                           product_id: |  | ||||||
|  *                             type: integer |  | ||||||
|  *                           product_name: |  | ||||||
|  *                             type: string |  | ||||||
|  *                           quantity: |  | ||||||
|  *                             type: integer |  | ||||||
|  *                           points_price: |  | ||||||
|  *                             type: integer |  | ||||||
|  *                           rongdou_price: |  | ||||||
|  *                             type: number |  | ||||||
|  *                           image_url: |  | ||||||
|  *                             type: string |  | ||||||
|  *       404: |  | ||||||
|  *         description: 预订单不存在 |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @swagger |  | ||||||
|  * /orders/confirm: |  | ||||||
|  *   post: |  | ||||||
|  *     summary: 确认下单 |  | ||||||
|  *     tags: [Orders] |  | ||||||
|  *     requestBody: |  | ||||||
|  *       required: true |  | ||||||
|  *       content: |  | ||||||
|  *         application/json: |  | ||||||
|  *           schema: |  | ||||||
|  *             type: object |  | ||||||
|  *             required: |  | ||||||
|  *               - preOrderId |  | ||||||
|  *               - shippingAddress |  | ||||||
|  *             properties: |  | ||||||
|  *               preOrderId: |  | ||||||
|  *                 type: integer |  | ||||||
|  *                 description: 预订单ID |  | ||||||
|  *               shippingAddress: |  | ||||||
|  *                 type: string |  | ||||||
|  *                 description: 收货地址 |  | ||||||
|  *     responses: |  | ||||||
|  *       200: |  | ||||||
|  *         description: 订单确认成功 |  | ||||||
|  *         content: |  | ||||||
|  *           application/json: |  | ||||||
|  *             schema: |  | ||||||
|  *               type: object |  | ||||||
|  *               properties: |  | ||||||
|  *                 success: |  | ||||||
|  *                   type: boolean |  | ||||||
|  *                 data: |  | ||||||
|  *                   type: object |  | ||||||
|  *                   properties: |  | ||||||
|  *                     orderId: |  | ||||||
|  *                       type: integer |  | ||||||
|  *                     orderNumber: |  | ||||||
|  *                       type: string |  | ||||||
|  *       400: |  | ||||||
|  *         description: 请求参数错误或余额不足 |  | ||||||
|  *       404: |  | ||||||
|  *         description: 预订单不存在 |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @swagger |  | ||||||
|  * /orders: |  | ||||||
|  *   get: |  | ||||||
|  *     summary: 获取用户订单列表 |  | ||||||
|  *     tags: [Orders] |  *     tags: [Orders] | ||||||
|  |  *     security: | ||||||
|  |  *       - bearerAuth: [] | ||||||
|  *     parameters: |  *     parameters: | ||||||
|  *       - in: query |  *       - in: query | ||||||
|  *         name: page |  *         name: page | ||||||
| @@ -164,11 +27,37 @@ | |||||||
|  *           default: 10 |  *           default: 10 | ||||||
|  *         description: 每页数量 |  *         description: 每页数量 | ||||||
|  *       - in: query |  *       - in: query | ||||||
|  |  *         name: search | ||||||
|  |  *         schema: | ||||||
|  |  *           type: string | ||||||
|  |  *         description: 搜索关键词 | ||||||
|  |  *       - in: query | ||||||
|  |  *         name: orderNumber | ||||||
|  |  *         schema: | ||||||
|  |  *           type: string | ||||||
|  |  *         description: 订单号 | ||||||
|  |  *       - in: query | ||||||
|  |  *         name: username | ||||||
|  |  *         schema: | ||||||
|  |  *           type: string | ||||||
|  |  *         description: 用户名 | ||||||
|  |  *       - in: query | ||||||
|  *         name: status |  *         name: status | ||||||
|  *         schema: |  *         schema: | ||||||
|  *           type: string |  *           type: string | ||||||
|  *           enum: [pending, confirmed, shipped, delivered, cancelled] |  *         description: 订单状态 | ||||||
|  *         description: 订单状态筛选 |  *       - in: query | ||||||
|  |  *         name: startDate | ||||||
|  |  *         schema: | ||||||
|  |  *           type: string | ||||||
|  |  *           format: date | ||||||
|  |  *         description: 开始日期 | ||||||
|  |  *       - in: query | ||||||
|  |  *         name: endDate | ||||||
|  |  *         schema: | ||||||
|  |  *           type: string | ||||||
|  |  *           format: date | ||||||
|  |  *         description: 结束日期 | ||||||
|  *     responses: |  *     responses: | ||||||
|  *       200: |  *       200: | ||||||
|  *         description: 成功获取订单列表 |  *         description: 成功获取订单列表 | ||||||
| @@ -197,14 +86,162 @@ | |||||||
|  *                           type: integer |  *                           type: integer | ||||||
|  *                         pages: |  *                         pages: | ||||||
|  *                           type: integer |  *                           type: integer | ||||||
|  |  *       401: | ||||||
|  |  *         description: 未授权 | ||||||
|  |  *       500: | ||||||
|  |  *         description: 服务器错误 | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @swagger |  * @swagger | ||||||
|  * /orders/{id}: |  * /api/orders/confirm: | ||||||
|  *   get: |  *   post: | ||||||
|  *     summary: 获取订单详情 |  *     summary: 确认下单 | ||||||
|  *     tags: [Orders] |  *     tags: [Orders] | ||||||
|  |  *     security: | ||||||
|  |  *       - bearerAuth: [] | ||||||
|  |  *     requestBody: | ||||||
|  |  *       required: true | ||||||
|  |  *       content: | ||||||
|  |  *         application/json: | ||||||
|  |  *           schema: | ||||||
|  |  *             type: object | ||||||
|  |  *             required: | ||||||
|  |  *               - pre_order_id | ||||||
|  |  *               - address | ||||||
|  |  *             properties: | ||||||
|  |  *               pre_order_id: | ||||||
|  |  *                 type: integer | ||||||
|  |  *                 description: 预订单ID | ||||||
|  |  *               address: | ||||||
|  |  *                 type: object | ||||||
|  |  *                 properties: | ||||||
|  |  *                   recipient_name: | ||||||
|  |  *                     type: string | ||||||
|  |  *                     description: 收货人姓名 | ||||||
|  |  *                   phone: | ||||||
|  |  *                     type: string | ||||||
|  |  *                     description: 收货人电话 | ||||||
|  |  *                   province: | ||||||
|  |  *                     type: string | ||||||
|  |  *                     description: 省份 | ||||||
|  |  *                   city: | ||||||
|  |  *                     type: string | ||||||
|  |  *                     description: 城市 | ||||||
|  |  *                   district: | ||||||
|  |  *                     type: string | ||||||
|  |  *                     description: 区县 | ||||||
|  |  *                   detail_address: | ||||||
|  |  *                     type: string | ||||||
|  |  *                     description: 详细地址 | ||||||
|  |  *     responses: | ||||||
|  |  *       200: | ||||||
|  |  *         description: 确认下单成功 | ||||||
|  |  *         content: | ||||||
|  |  *           application/json: | ||||||
|  |  *             schema: | ||||||
|  |  *               type: object | ||||||
|  |  *               properties: | ||||||
|  |  *                 success: | ||||||
|  |  *                   type: boolean | ||||||
|  |  *                 message: | ||||||
|  |  *                   type: string | ||||||
|  |  *                 data: | ||||||
|  |  *                   type: object | ||||||
|  |  *                   properties: | ||||||
|  |  *                     order_id: | ||||||
|  |  *                       type: integer | ||||||
|  |  *                     order_no: | ||||||
|  |  *                       type: string | ||||||
|  |  *       400: | ||||||
|  |  *         description: 请求参数错误 | ||||||
|  |  *       401: | ||||||
|  |  *         description: 未授权 | ||||||
|  |  *       404: | ||||||
|  |  *         description: 预订单不存在 | ||||||
|  |  *       500: | ||||||
|  |  *         description: 服务器错误 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @swagger | ||||||
|  |  * /api/orders/pre-order/{id}: | ||||||
|  |  *   get: | ||||||
|  |  *     summary: 获取预订单详情 | ||||||
|  |  *     tags: [Orders] | ||||||
|  |  *     security: | ||||||
|  |  *       - bearerAuth: [] | ||||||
|  |  *     parameters: | ||||||
|  |  *       - in: path | ||||||
|  |  *         name: id | ||||||
|  |  *         required: true | ||||||
|  |  *         schema: | ||||||
|  |  *           type: integer | ||||||
|  |  *         description: 预订单ID | ||||||
|  |  *     responses: | ||||||
|  |  *       200: | ||||||
|  |  *         description: 获取预订单详情成功 | ||||||
|  |  *         content: | ||||||
|  |  *           application/json: | ||||||
|  |  *             schema: | ||||||
|  |  *               type: object | ||||||
|  |  *               properties: | ||||||
|  |  *                 success: | ||||||
|  |  *                   type: boolean | ||||||
|  |  *                 data: | ||||||
|  |  *                   type: object | ||||||
|  |  *                   properties: | ||||||
|  |  *                     id: | ||||||
|  |  *                       type: integer | ||||||
|  |  *                     order_no: | ||||||
|  |  *                       type: string | ||||||
|  |  *                     total_amount: | ||||||
|  |  *                       type: integer | ||||||
|  |  *                     total_points: | ||||||
|  |  *                       type: integer | ||||||
|  |  *                     total_rongdou: | ||||||
|  |  *                       type: integer | ||||||
|  |  *                     status: | ||||||
|  |  *                       type: string | ||||||
|  |  *                     created_at: | ||||||
|  |  *                       type: string | ||||||
|  |  *                     items: | ||||||
|  |  *                       type: array | ||||||
|  |  *                       items: | ||||||
|  |  *                         type: object | ||||||
|  |  *                         properties: | ||||||
|  |  *                           id: | ||||||
|  |  *                             type: integer | ||||||
|  |  *                           product_id: | ||||||
|  |  *                             type: integer | ||||||
|  |  *                           product_name: | ||||||
|  |  *                             type: string | ||||||
|  |  *                           quantity: | ||||||
|  |  *                             type: integer | ||||||
|  |  *                           price: | ||||||
|  |  *                             type: integer | ||||||
|  |  *                           points_price: | ||||||
|  |  *                             type: integer | ||||||
|  |  *                           rongdou_price: | ||||||
|  |  *                             type: integer | ||||||
|  |  *                           spec_info: | ||||||
|  |  *                             type: object | ||||||
|  |  *       401: | ||||||
|  |  *         description: 未授权 | ||||||
|  |  *       404: | ||||||
|  |  *         description: 预订单不存在 | ||||||
|  |  *       500: | ||||||
|  |  *         description: 服务器错误 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @swagger | ||||||
|  |  * /api/orders/{id}: | ||||||
|  |  *   get: | ||||||
|  |  *     summary: 获取单个订单详情 | ||||||
|  |  *     tags: [Orders] | ||||||
|  |  *     security: | ||||||
|  |  *       - bearerAuth: [] | ||||||
|  *     parameters: |  *     parameters: | ||||||
|  *       - in: path |  *       - in: path | ||||||
|  *         name: id |  *         name: id | ||||||
| @@ -227,10 +264,10 @@ | |||||||
|  *                   properties: |  *                   properties: | ||||||
|  *                     order: |  *                     order: | ||||||
|  *                       $ref: '#/components/schemas/Order' |  *                       $ref: '#/components/schemas/Order' | ||||||
|  *                     items: |  *       401: | ||||||
|  *                       type: array |  *         description: 未授权 | ||||||
|  *                       items: |  | ||||||
|  *                         $ref: '#/components/schemas/OrderItem' |  | ||||||
|  *       404: |  *       404: | ||||||
|  *         description: 订单不存在 |  *         description: 订单不存在 | ||||||
|  |  *       500: | ||||||
|  |  *         description: 服务器错误 | ||||||
|  */ |  */ | ||||||
							
								
								
									
										225
									
								
								docs/schemas/announcement.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								docs/schemas/announcement.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,225 @@ | |||||||
|  | /** | ||||||
|  |  * @swagger | ||||||
|  |  * components: | ||||||
|  |  *   schemas: | ||||||
|  |  *     Announcement: | ||||||
|  |  *       type: object | ||||||
|  |  *       required: | ||||||
|  |  *         - title | ||||||
|  |  *         - content | ||||||
|  |  *         - type | ||||||
|  |  *         - priority | ||||||
|  |  *       properties: | ||||||
|  |  *         id: | ||||||
|  |  *           type: integer | ||||||
|  |  *           description: 公告ID | ||||||
|  |  *           example: 1 | ||||||
|  |  *         title: | ||||||
|  |  *           type: string | ||||||
|  |  *           description: 公告标题 | ||||||
|  |  *           example: "系统维护通知" | ||||||
|  |  *         content: | ||||||
|  |  *           type: string | ||||||
|  |  *           description: 公告内容 | ||||||
|  |  *           example: "系统将于今晚进行维护,预计维护时间2小时,期间可能影响部分功能使用。" | ||||||
|  |  *         type: | ||||||
|  |  *           type: string | ||||||
|  |  *           description: 公告类型 | ||||||
|  |  *           enum: [system, activity, maintenance, urgent] | ||||||
|  |  *           example: "maintenance" | ||||||
|  |  *         priority: | ||||||
|  |  *           type: string | ||||||
|  |  *           description: 优先级 | ||||||
|  |  *           enum: [high, medium, low] | ||||||
|  |  *           example: "high" | ||||||
|  |  *         status: | ||||||
|  |  *           type: string | ||||||
|  |  *           description: 公告状态 | ||||||
|  |  *           enum: [draft, published, expired] | ||||||
|  |  *           example: "published" | ||||||
|  |  *         isTop: | ||||||
|  |  *           type: boolean | ||||||
|  |  *           description: 是否置顶 | ||||||
|  |  *           example: false | ||||||
|  |  *         publishTime: | ||||||
|  |  *           type: string | ||||||
|  |  *           format: date-time | ||||||
|  |  *           description: 发布时间 | ||||||
|  |  *           example: "2024-01-15T10:00:00Z" | ||||||
|  |  *         expireTime: | ||||||
|  |  *           type: string | ||||||
|  |  *           format: date-time | ||||||
|  |  *           description: 过期时间 | ||||||
|  |  *           example: "2024-01-20T10:00:00Z" | ||||||
|  |  *         createdBy: | ||||||
|  |  *           type: integer | ||||||
|  |  *           description: 创建者用户ID | ||||||
|  |  *           example: 1 | ||||||
|  |  *         createdAt: | ||||||
|  |  *           type: string | ||||||
|  |  *           format: date-time | ||||||
|  |  *           description: 创建时间 | ||||||
|  |  *           example: "2024-01-15T09:00:00Z" | ||||||
|  |  *         updatedAt: | ||||||
|  |  *           type: string | ||||||
|  |  *           format: date-time | ||||||
|  |  *           description: 更新时间 | ||||||
|  |  *           example: "2024-01-15T09:30:00Z" | ||||||
|  |  *         creator: | ||||||
|  |  *           type: object | ||||||
|  |  *           description: 创建者信息 | ||||||
|  |  *           properties: | ||||||
|  |  *             id: | ||||||
|  |  *               type: integer | ||||||
|  |  *               example: 1 | ||||||
|  |  *             username: | ||||||
|  |  *               type: string | ||||||
|  |  *               example: "admin" | ||||||
|  |  *             email: | ||||||
|  |  *               type: string | ||||||
|  |  *               example: "admin@example.com" | ||||||
|  |  * | ||||||
|  |  *     AnnouncementCreate: | ||||||
|  |  *       type: object | ||||||
|  |  *       required: | ||||||
|  |  *         - title | ||||||
|  |  *         - content | ||||||
|  |  *         - type | ||||||
|  |  *         - priority | ||||||
|  |  *       properties: | ||||||
|  |  *         title: | ||||||
|  |  *           type: string | ||||||
|  |  *           description: 公告标题 | ||||||
|  |  *           example: "系统维护通知" | ||||||
|  |  *         content: | ||||||
|  |  *           type: string | ||||||
|  |  *           description: 公告内容 | ||||||
|  |  *           example: "系统将于今晚进行维护,预计维护时间2小时。" | ||||||
|  |  *         type: | ||||||
|  |  *           type: string | ||||||
|  |  *           description: 公告类型 | ||||||
|  |  *           enum: [system, activity, maintenance, urgent] | ||||||
|  |  *           example: "maintenance" | ||||||
|  |  *         priority: | ||||||
|  |  *           type: string | ||||||
|  |  *           description: 优先级 | ||||||
|  |  *           enum: [high, medium, low] | ||||||
|  |  *           example: "high" | ||||||
|  |  *         status: | ||||||
|  |  *           type: string | ||||||
|  |  *           description: 公告状态 | ||||||
|  |  *           enum: [draft, published] | ||||||
|  |  *           default: draft | ||||||
|  |  *           example: "draft" | ||||||
|  |  *         isTop: | ||||||
|  |  *           type: boolean | ||||||
|  |  *           description: 是否置顶 | ||||||
|  |  *           default: false | ||||||
|  |  *           example: false | ||||||
|  |  *         publishTime: | ||||||
|  |  *           type: string | ||||||
|  |  *           format: date-time | ||||||
|  |  *           description: 发布时间 | ||||||
|  |  *           example: "2024-01-15T10:00:00Z" | ||||||
|  |  *         expireTime: | ||||||
|  |  *           type: string | ||||||
|  |  *           format: date-time | ||||||
|  |  *           description: 过期时间 | ||||||
|  |  *           example: "2024-01-20T10:00:00Z" | ||||||
|  |  * | ||||||
|  |  *     AnnouncementUpdate: | ||||||
|  |  *       type: object | ||||||
|  |  *       properties: | ||||||
|  |  *         title: | ||||||
|  |  *           type: string | ||||||
|  |  *           description: 公告标题 | ||||||
|  |  *           example: "系统维护通知(更新)" | ||||||
|  |  *         content: | ||||||
|  |  *           type: string | ||||||
|  |  *           description: 公告内容 | ||||||
|  |  *           example: "系统维护时间调整为明晚进行。" | ||||||
|  |  *         type: | ||||||
|  |  *           type: string | ||||||
|  |  *           description: 公告类型 | ||||||
|  |  *           enum: [system, activity, maintenance, urgent] | ||||||
|  |  *           example: "maintenance" | ||||||
|  |  *         priority: | ||||||
|  |  *           type: string | ||||||
|  |  *           description: 优先级 | ||||||
|  |  *           enum: [high, medium, low] | ||||||
|  |  *           example: "medium" | ||||||
|  |  *         status: | ||||||
|  |  *           type: string | ||||||
|  |  *           description: 公告状态 | ||||||
|  |  *           enum: [draft, published, expired] | ||||||
|  |  *           example: "published" | ||||||
|  |  *         isTop: | ||||||
|  |  *           type: boolean | ||||||
|  |  *           description: 是否置顶 | ||||||
|  |  *           example: true | ||||||
|  |  *         publishTime: | ||||||
|  |  *           type: string | ||||||
|  |  *           format: date-time | ||||||
|  |  *           description: 发布时间 | ||||||
|  |  *           example: "2024-01-16T10:00:00Z" | ||||||
|  |  *         expireTime: | ||||||
|  |  *           type: string | ||||||
|  |  *           format: date-time | ||||||
|  |  *           description: 过期时间 | ||||||
|  |  *           example: "2024-01-21T10:00:00Z" | ||||||
|  |  * | ||||||
|  |  *     AnnouncementList: | ||||||
|  |  *       type: object | ||||||
|  |  *       properties: | ||||||
|  |  *         success: | ||||||
|  |  *           type: boolean | ||||||
|  |  *           example: true | ||||||
|  |  *         data: | ||||||
|  |  *           type: object | ||||||
|  |  *           properties: | ||||||
|  |  *             announcements: | ||||||
|  |  *               type: array | ||||||
|  |  *               items: | ||||||
|  |  *                 $ref: '#/components/schemas/Announcement' | ||||||
|  |  *             total: | ||||||
|  |  *               type: integer | ||||||
|  |  *               description: 总记录数 | ||||||
|  |  *               example: 50 | ||||||
|  |  *             page: | ||||||
|  |  *               type: integer | ||||||
|  |  *               description: 当前页码 | ||||||
|  |  *               example: 1 | ||||||
|  |  *             limit: | ||||||
|  |  *               type: integer | ||||||
|  |  *               description: 每页数量 | ||||||
|  |  *               example: 10 | ||||||
|  |  *             totalPages: | ||||||
|  |  *               type: integer | ||||||
|  |  *               description: 总页数 | ||||||
|  |  *               example: 5 | ||||||
|  |  * | ||||||
|  |  *     AnnouncementResponse: | ||||||
|  |  *       type: object | ||||||
|  |  *       properties: | ||||||
|  |  *         success: | ||||||
|  |  *           type: boolean | ||||||
|  |  *           example: true | ||||||
|  |  *         message: | ||||||
|  |  *           type: string | ||||||
|  |  *           example: "操作成功" | ||||||
|  |  *         data: | ||||||
|  |  *           $ref: '#/components/schemas/Announcement' | ||||||
|  |  * | ||||||
|  |  *     AnnouncementError: | ||||||
|  |  *       type: object | ||||||
|  |  *       properties: | ||||||
|  |  *         success: | ||||||
|  |  *           type: boolean | ||||||
|  |  *           example: false | ||||||
|  |  *         message: | ||||||
|  |  *           type: string | ||||||
|  |  *           example: "操作失败" | ||||||
|  |  *         error: | ||||||
|  |  *           type: string | ||||||
|  |  *           example: "公告不存在" | ||||||
|  |  */ | ||||||
| @@ -34,6 +34,19 @@ const auth = async (req, res, next) => { | |||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     // 检查支付状态(管理员除外) | ||||||
|  |     if (user.role !== 'admin' && user.payment_status === 'unpaid') { | ||||||
|  |       console.log(11111); | ||||||
|  |        | ||||||
|  |       return res.status(403).json({  | ||||||
|  |         success: false,  | ||||||
|  |         message: '您的账户尚未激活,请完成支付后再使用', | ||||||
|  |         code: 'PAYMENT_REQUIRED', | ||||||
|  |         needPayment: true, | ||||||
|  |         userId: user.id | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     req.user = user; |     req.user = user; | ||||||
|     next(); |     next(); | ||||||
|   } catch (error) { |   } catch (error) { | ||||||
| @@ -49,4 +62,51 @@ const adminAuth = (req, res, next) => { | |||||||
|   next(); |   next(); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| module.exports = { auth, adminAuth, JWT_SECRET }; | /** | ||||||
|  |  * 支付认证中间件 | ||||||
|  |  * 只验证JWT令牌和用户状态,不检查支付状态 | ||||||
|  |  * 用于支付相关接口,允许未支付用户创建支付订单 | ||||||
|  |  */ | ||||||
|  | const paymentAuth = async (req, res, next) => { | ||||||
|  |   try { | ||||||
|  |     const token = req.header('Authorization')?.replace('Bearer ', ''); | ||||||
|  |      | ||||||
|  |     if (!token) { | ||||||
|  |       return res.status(401).json({ success: false, message: '未提供认证令牌' }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const decoded = jwt.verify(token, JWT_SECRET); | ||||||
|  |     const db = getDB(); | ||||||
|  |     const [users] = await db.execute('SELECT * FROM users WHERE id = ?', [decoded.userId]); | ||||||
|  |      | ||||||
|  |     if (users.length === 0) { | ||||||
|  |       return res.status(401).json({ success: false, message: '用户不存在' }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const user = users[0]; | ||||||
|  |      | ||||||
|  |     // 检查用户是否被拉黑 | ||||||
|  |     if (user.is_blacklisted) { | ||||||
|  |       return res.status(403).json({  | ||||||
|  |         success: false,  | ||||||
|  |         message: '账户已被拉黑,请联系管理员',  | ||||||
|  |         code: 'USER_BLACKLISTED'  | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // 注意:这里不检查支付状态,允许未支付用户创建支付订单 | ||||||
|  |     req.user = user; | ||||||
|  |     next(); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('支付认证失败:', error); | ||||||
|  |     if (error.name === 'JsonWebTokenError') { | ||||||
|  |       return res.status(401).json({ success: false, message: '无效的认证令牌' }); | ||||||
|  |     } | ||||||
|  |     if (error.name === 'TokenExpiredError') { | ||||||
|  |       return res.status(401).json({ success: false, message: '认证令牌已过期' }); | ||||||
|  |     } | ||||||
|  |     return res.status(500).json({ success: false, message: '认证失败' }); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | module.exports = { auth, adminAuth, paymentAuth, JWT_SECRET }; | ||||||
							
								
								
									
										17
									
								
								migrations/add_alipay_support.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								migrations/add_alipay_support.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | -- 为支付订单表添加支付宝支持 | ||||||
|  | -- 更新交易类型枚举,添加支付宝相关类型 | ||||||
|  | ALTER TABLE `payment_orders` MODIFY COLUMN `trade_type` varchar(32) NOT NULL COMMENT '交易类型(H5/JSAPI/ALIPAY_WAP/ALIPAY_APP等)'; | ||||||
|  |  | ||||||
|  | -- 更新transaction_id字段注释,支持多种支付方式 | ||||||
|  | ALTER TABLE `payment_orders` MODIFY COLUMN `transaction_id` varchar(64) DEFAULT NULL COMMENT '第三方支付订单号(微信/支付宝)'; | ||||||
|  |  | ||||||
|  | -- 添加支付方式字段 | ||||||
|  | ALTER TABLE `payment_orders` ADD COLUMN `payment_method` enum('wechat','alipay') DEFAULT 'wechat' COMMENT '支付方式' AFTER `trade_type`; | ||||||
|  |  | ||||||
|  | -- 添加支付宝特有字段 | ||||||
|  | ALTER TABLE `payment_orders` ADD COLUMN `buyer_user_id` varchar(32) DEFAULT NULL COMMENT '支付宝买家用户ID' AFTER `transaction_id`; | ||||||
|  | ALTER TABLE `payment_orders` ADD COLUMN `trade_status` varchar(32) DEFAULT NULL COMMENT '支付宝交易状态' AFTER `buyer_user_id`; | ||||||
|  |  | ||||||
|  | -- 添加索引 | ||||||
|  | ALTER TABLE `payment_orders` ADD KEY `idx_payment_method` (`payment_method`); | ||||||
|  | ALTER TABLE `payment_orders` ADD KEY `idx_trade_status` (`trade_status`); | ||||||
							
								
								
									
										27
									
								
								migrations/create_payment_orders_table.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								migrations/create_payment_orders_table.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | -- 创建支付订单表 | ||||||
|  | CREATE TABLE IF NOT EXISTS `payment_orders` ( | ||||||
|  |   `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '订单ID', | ||||||
|  |   `user_id` int(11) NOT NULL COMMENT '用户ID', | ||||||
|  |   `out_trade_no` varchar(64) NOT NULL COMMENT '商户订单号', | ||||||
|  |   `transaction_id` varchar(64) DEFAULT NULL COMMENT '微信支付订单号', | ||||||
|  |   `total_fee` int(11) NOT NULL COMMENT '订单金额(分)', | ||||||
|  |   `body` varchar(128) NOT NULL COMMENT '商品描述', | ||||||
|  |   `trade_type` varchar(16) NOT NULL COMMENT '交易类型', | ||||||
|  |   `prepay_id` varchar(64) DEFAULT NULL COMMENT '预支付交易会话标识', | ||||||
|  |   `mweb_url` text DEFAULT NULL COMMENT 'H5支付跳转链接', | ||||||
|  |   `status` enum('pending','paid','failed','cancelled') NOT NULL DEFAULT 'pending' COMMENT '支付状态', | ||||||
|  |   `paid_at` datetime DEFAULT NULL COMMENT '支付完成时间', | ||||||
|  |   `created_at` datetime NOT NULL COMMENT '创建时间', | ||||||
|  |   `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', | ||||||
|  |   PRIMARY KEY (`id`), | ||||||
|  |   UNIQUE KEY `uk_out_trade_no` (`out_trade_no`), | ||||||
|  |   KEY `idx_user_id` (`user_id`), | ||||||
|  |   KEY `idx_status` (`status`), | ||||||
|  |   KEY `idx_created_at` (`created_at`) | ||||||
|  | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付订单表'; | ||||||
|  |  | ||||||
|  | -- 为用户表添加支付状态字段 | ||||||
|  | ALTER TABLE `users` ADD COLUMN `payment_status` enum('unpaid','paid') NOT NULL DEFAULT 'unpaid' COMMENT '支付状态' AFTER `status`; | ||||||
|  |  | ||||||
|  | -- 添加索引 | ||||||
|  | ALTER TABLE `users` ADD KEY `idx_payment_status` (`payment_status`); | ||||||
							
								
								
									
										381
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										381
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -11,10 +11,12 @@ | |||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@alicloud/dysmsapi20170525": "^4.1.2", |         "@alicloud/dysmsapi20170525": "^4.1.2", | ||||||
|         "@alicloud/openapi-client": "^0.4.15", |         "@alicloud/openapi-client": "^0.4.15", | ||||||
|  |         "alipay-sdk": "^4.14.0", | ||||||
|         "axios": "^1.11.0", |         "axios": "^1.11.0", | ||||||
|         "bcryptjs": "^2.4.3", |         "bcryptjs": "^2.4.3", | ||||||
|         "body-parser": "^1.20.2", |         "body-parser": "^1.20.2", | ||||||
|         "cors": "^2.8.5", |         "cors": "^2.8.5", | ||||||
|  |         "crypto": "^1.0.1", | ||||||
|         "dayjs": "^1.11.13", |         "dayjs": "^1.11.13", | ||||||
|         "dotenv": "^17.2.1", |         "dotenv": "^17.2.1", | ||||||
|         "express": "^4.18.2", |         "express": "^4.18.2", | ||||||
| @@ -27,10 +29,12 @@ | |||||||
|         "multer": "^1.4.5-lts.1", |         "multer": "^1.4.5-lts.1", | ||||||
|         "mysql2": "^3.14.3", |         "mysql2": "^3.14.3", | ||||||
|         "node-cron": "^4.2.1", |         "node-cron": "^4.2.1", | ||||||
|  |         "node-rsa": "^1.1.1", | ||||||
|         "qrcode": "^1.5.4", |         "qrcode": "^1.5.4", | ||||||
|         "swagger-jsdoc": "^6.2.8", |         "swagger-jsdoc": "^6.2.8", | ||||||
|         "swagger-ui-express": "^5.0.1", |         "swagger-ui-express": "^5.0.1", | ||||||
|         "winston": "^3.17.0" |         "winston": "^3.17.0", | ||||||
|  |         "xml2js": "^0.6.2" | ||||||
|       }, |       }, | ||||||
|       "devDependencies": { |       "devDependencies": { | ||||||
|         "concurrently": "^8.2.2", |         "concurrently": "^8.2.2", | ||||||
| @@ -311,6 +315,28 @@ | |||||||
|         "xml2js": "^0.6.2" |         "xml2js": "^0.6.2" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/@fidm/asn1": { | ||||||
|  |       "version": "1.0.4", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@fidm/asn1/-/asn1-1.0.4.tgz", | ||||||
|  |       "integrity": "sha512-esd1jyNvRb2HVaQGq2Gg8Z0kbQPXzV9Tq5Z14KNIov6KfFD6PTaRIO8UpcsYiTNzOqJpmyzWgVTrUwFV3UF4TQ==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">= 8" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/@fidm/x509": { | ||||||
|  |       "version": "1.2.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@fidm/x509/-/x509-1.2.1.tgz", | ||||||
|  |       "integrity": "sha512-nwc2iesjyc9hkuzcrMCBXQRn653XuAUKorfWM8PZyJawiy1QzLj4vahwzaI25+pfpwOLvMzbJ0uKpWLDNmo16w==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "@fidm/asn1": "^1.0.4", | ||||||
|  |         "tweetnacl": "^1.0.1" | ||||||
|  |       }, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">= 8" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/@hapi/hoek": { |     "node_modules/@hapi/hoek": { | ||||||
|       "version": "9.3.0", |       "version": "9.3.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", |       "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", | ||||||
| @@ -407,6 +433,26 @@ | |||||||
|         "node": ">= 0.6" |         "node": ">= 0.6" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/alipay-sdk": { | ||||||
|  |       "version": "4.14.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/alipay-sdk/-/alipay-sdk-4.14.0.tgz", | ||||||
|  |       "integrity": "sha512-oiD/VP5Ei0RRacHHmE+N0uqgOj2xzce7c0fHrtyyh1P04O+o9I1r65LdGPzU8960J56xOxS/d3c+R/9lsPUH7g==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "@fidm/x509": "^1.2.1", | ||||||
|  |         "bignumber.js": "^9.1.2", | ||||||
|  |         "camelcase-keys": "^7.0.2", | ||||||
|  |         "crypto-js": "^4.2.0", | ||||||
|  |         "formstream": "^1.5.0", | ||||||
|  |         "snakecase-keys": "^8.0.0", | ||||||
|  |         "sse-decoder": "^1.0.0", | ||||||
|  |         "urllib": "^4", | ||||||
|  |         "utility": "^2.1.0" | ||||||
|  |       }, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=18.0.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/ansi-regex": { |     "node_modules/ansi-regex": { | ||||||
|       "version": "5.0.1", |       "version": "5.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", |       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", | ||||||
| @@ -463,6 +509,15 @@ | |||||||
|       "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", |       "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", | ||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/asn1": { | ||||||
|  |       "version": "0.2.6", | ||||||
|  |       "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", | ||||||
|  |       "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "safer-buffer": "~2.1.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/async": { |     "node_modules/async": { | ||||||
|       "version": "3.2.6", |       "version": "3.2.6", | ||||||
|       "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", |       "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", | ||||||
| @@ -522,6 +577,15 @@ | |||||||
|       "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", |       "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", | ||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/bignumber.js": { | ||||||
|  |       "version": "9.3.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", | ||||||
|  |       "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "engines": { | ||||||
|  |         "node": "*" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/binary-extensions": { |     "node_modules/binary-extensions": { | ||||||
|       "version": "2.3.0", |       "version": "2.3.0", | ||||||
|       "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", |       "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", | ||||||
| @@ -714,6 +778,36 @@ | |||||||
|         "node": ">=6" |         "node": ">=6" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/camelcase-keys": { | ||||||
|  |       "version": "7.0.2", | ||||||
|  |       "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-7.0.2.tgz", | ||||||
|  |       "integrity": "sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "camelcase": "^6.3.0", | ||||||
|  |         "map-obj": "^4.1.0", | ||||||
|  |         "quick-lru": "^5.1.1", | ||||||
|  |         "type-fest": "^1.2.1" | ||||||
|  |       }, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=12" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "url": "https://github.com/sponsors/sindresorhus" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/camelcase-keys/node_modules/camelcase": { | ||||||
|  |       "version": "6.3.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", | ||||||
|  |       "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=10" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "url": "https://github.com/sponsors/sindresorhus" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/chalk": { |     "node_modules/chalk": { | ||||||
|       "version": "4.1.2", |       "version": "4.1.2", | ||||||
|       "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", |       "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", | ||||||
| @@ -972,6 +1066,19 @@ | |||||||
|         "node": ">= 0.10" |         "node": ">= 0.10" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/crypto": { | ||||||
|  |       "version": "1.0.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", | ||||||
|  |       "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", | ||||||
|  |       "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", | ||||||
|  |       "license": "ISC" | ||||||
|  |     }, | ||||||
|  |     "node_modules/crypto-js": { | ||||||
|  |       "version": "4.2.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", | ||||||
|  |       "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", | ||||||
|  |       "license": "MIT" | ||||||
|  |     }, | ||||||
|     "node_modules/date-fns": { |     "node_modules/date-fns": { | ||||||
|       "version": "2.30.0", |       "version": "2.30.0", | ||||||
|       "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", |       "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", | ||||||
| @@ -1094,6 +1201,16 @@ | |||||||
|         "node": ">=6.0.0" |         "node": ">=6.0.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/dot-case": { | ||||||
|  |       "version": "3.0.4", | ||||||
|  |       "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", | ||||||
|  |       "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "no-case": "^3.0.4", | ||||||
|  |         "tslib": "^2.0.3" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/dotenv": { |     "node_modules/dotenv": { | ||||||
|       "version": "17.2.1", |       "version": "17.2.1", | ||||||
|       "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", |       "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", | ||||||
| @@ -1315,6 +1432,18 @@ | |||||||
|         "node": ">= 8.0.0" |         "node": ">= 8.0.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/extend-shallow": { | ||||||
|  |       "version": "2.0.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", | ||||||
|  |       "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "is-extendable": "^0.1.0" | ||||||
|  |       }, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=0.10.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/fast-xml-parser": { |     "node_modules/fast-xml-parser": { | ||||||
|       "version": "4.5.3", |       "version": "4.5.3", | ||||||
|       "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", |       "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", | ||||||
| @@ -1449,6 +1578,30 @@ | |||||||
|         "node": ">= 6" |         "node": ">= 6" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/formstream": { | ||||||
|  |       "version": "1.5.2", | ||||||
|  |       "resolved": "https://registry.npmjs.org/formstream/-/formstream-1.5.2.tgz", | ||||||
|  |       "integrity": "sha512-NASf0lgxC1AyKNXQIrXTEYkiX99LhCEXTkiGObXAkpBui86a4u8FjH1o2bGb3PpqI3kafC+yw4zWeK6l6VHTgg==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "destroy": "^1.0.4", | ||||||
|  |         "mime": "^2.5.2", | ||||||
|  |         "node-hex": "^1.0.1", | ||||||
|  |         "pause-stream": "~0.0.11" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/formstream/node_modules/mime": { | ||||||
|  |       "version": "2.6.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", | ||||||
|  |       "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "bin": { | ||||||
|  |         "mime": "cli.js" | ||||||
|  |       }, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=4.0.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/forwarded": { |     "node_modules/forwarded": { | ||||||
|       "version": "0.2.0", |       "version": "0.2.0", | ||||||
|       "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", |       "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", | ||||||
| @@ -1824,6 +1977,15 @@ | |||||||
|         "url": "https://github.com/sponsors/ljharb" |         "url": "https://github.com/sponsors/ljharb" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/is-extendable": { | ||||||
|  |       "version": "0.1.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", | ||||||
|  |       "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=0.10.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/is-extglob": { |     "node_modules/is-extglob": { | ||||||
|       "version": "2.1.1", |       "version": "2.1.1", | ||||||
|       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", |       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", | ||||||
| @@ -2148,6 +2310,15 @@ | |||||||
|       "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", |       "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", | ||||||
|       "license": "Apache-2.0" |       "license": "Apache-2.0" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/lower-case": { | ||||||
|  |       "version": "2.0.2", | ||||||
|  |       "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", | ||||||
|  |       "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "tslib": "^2.0.3" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/lru-cache": { |     "node_modules/lru-cache": { | ||||||
|       "version": "7.18.3", |       "version": "7.18.3", | ||||||
|       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", |       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", | ||||||
| @@ -2172,6 +2343,18 @@ | |||||||
|         "url": "https://github.com/sponsors/wellwelwel" |         "url": "https://github.com/sponsors/wellwelwel" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/map-obj": { | ||||||
|  |       "version": "4.3.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", | ||||||
|  |       "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=8" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "url": "https://github.com/sponsors/sindresorhus" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/math-intrinsics": { |     "node_modules/math-intrinsics": { | ||||||
|       "version": "1.1.0", |       "version": "1.1.0", | ||||||
|       "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", |       "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", | ||||||
| @@ -2407,6 +2590,16 @@ | |||||||
|         "node": ">= 0.6" |         "node": ">= 0.6" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/no-case": { | ||||||
|  |       "version": "3.0.4", | ||||||
|  |       "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", | ||||||
|  |       "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "lower-case": "^2.0.2", | ||||||
|  |         "tslib": "^2.0.3" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/node-cron": { |     "node_modules/node-cron": { | ||||||
|       "version": "4.2.1", |       "version": "4.2.1", | ||||||
|       "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", |       "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", | ||||||
| @@ -2416,6 +2609,24 @@ | |||||||
|         "node": ">=6.0.0" |         "node": ">=6.0.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/node-hex": { | ||||||
|  |       "version": "1.0.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/node-hex/-/node-hex-1.0.1.tgz", | ||||||
|  |       "integrity": "sha512-iwpZdvW6Umz12ICmu9IYPRxg0tOLGmU3Tq2tKetejCj3oZd7b2nUXwP3a7QA5M9glWy8wlPS1G3RwM/CdsUbdQ==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=8.0.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/node-rsa": { | ||||||
|  |       "version": "1.1.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz", | ||||||
|  |       "integrity": "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "asn1": "^0.2.4" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/nodemon": { |     "node_modules/nodemon": { | ||||||
|       "version": "3.1.10", |       "version": "3.1.10", | ||||||
|       "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", |       "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", | ||||||
| @@ -2630,6 +2841,18 @@ | |||||||
|       "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", |       "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", | ||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/pause-stream": { | ||||||
|  |       "version": "0.0.11", | ||||||
|  |       "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", | ||||||
|  |       "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", | ||||||
|  |       "license": [ | ||||||
|  |         "MIT", | ||||||
|  |         "Apache2" | ||||||
|  |       ], | ||||||
|  |       "dependencies": { | ||||||
|  |         "through": "~2.3" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/picomatch": { |     "node_modules/picomatch": { | ||||||
|       "version": "2.3.1", |       "version": "2.3.1", | ||||||
|       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", |       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", | ||||||
| @@ -2809,6 +3032,18 @@ | |||||||
|         "url": "https://github.com/sponsors/sindresorhus" |         "url": "https://github.com/sponsors/sindresorhus" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/quick-lru": { | ||||||
|  |       "version": "5.1.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", | ||||||
|  |       "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=10" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "url": "https://github.com/sponsors/sindresorhus" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/range-parser": { |     "node_modules/range-parser": { | ||||||
|       "version": "1.2.1", |       "version": "1.2.1", | ||||||
|       "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", |       "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", | ||||||
| @@ -3163,6 +3398,42 @@ | |||||||
|       "integrity": "sha512-KyFkIfr8QBlFG3uc3NaljaXdYcsbRy1KrSfc4tsQV8jW68jAktGeOcifu530Vx/5LC+PULHT0Rv8LiI8Gw+c1g==", |       "integrity": "sha512-KyFkIfr8QBlFG3uc3NaljaXdYcsbRy1KrSfc4tsQV8jW68jAktGeOcifu530Vx/5LC+PULHT0Rv8LiI8Gw+c1g==", | ||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/snake-case": { | ||||||
|  |       "version": "3.0.4", | ||||||
|  |       "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", | ||||||
|  |       "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "dot-case": "^3.0.4", | ||||||
|  |         "tslib": "^2.0.3" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/snakecase-keys": { | ||||||
|  |       "version": "8.1.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-8.1.0.tgz", | ||||||
|  |       "integrity": "sha512-9/Eug2btrCiOi+9+vIXJnxUcKVfcbLy5Uwff4BrO6PQf3Oq/2iYQ/1zkmnrpIIjfel/DAasAlux7OvAmCa+Xnw==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "map-obj": "^4.2.0", | ||||||
|  |         "snake-case": "^3.0.4", | ||||||
|  |         "type-fest": "^4.15.0" | ||||||
|  |       }, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=18" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/snakecase-keys/node_modules/type-fest": { | ||||||
|  |       "version": "4.41.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", | ||||||
|  |       "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", | ||||||
|  |       "license": "(MIT OR CC0-1.0)", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=16" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "url": "https://github.com/sponsors/sindresorhus" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/spawn-command": { |     "node_modules/spawn-command": { | ||||||
|       "version": "0.0.2", |       "version": "0.0.2", | ||||||
|       "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", |       "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", | ||||||
| @@ -3187,6 +3458,15 @@ | |||||||
|         "node": ">= 0.6" |         "node": ">= 0.6" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/sse-decoder": { | ||||||
|  |       "version": "1.0.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/sse-decoder/-/sse-decoder-1.0.0.tgz", | ||||||
|  |       "integrity": "sha512-JPopy3jfNmPcUz5Ru6skKhHNRJbsvcEW6Z4SirKkucLS8Jya1Bmf4FVX8giOkLm8xQJ7kK68P6GXoVSTkbedUA==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">= 14.19.3" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/stack-trace": { |     "node_modules/stack-trace": { | ||||||
|       "version": "0.0.10", |       "version": "0.0.10", | ||||||
|       "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", |       "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", | ||||||
| @@ -3368,6 +3648,12 @@ | |||||||
|       "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", |       "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", | ||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/through": { | ||||||
|  |       "version": "2.3.8", | ||||||
|  |       "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", | ||||||
|  |       "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", | ||||||
|  |       "license": "MIT" | ||||||
|  |     }, | ||||||
|     "node_modules/through2": { |     "node_modules/through2": { | ||||||
|       "version": "4.0.2", |       "version": "4.0.2", | ||||||
|       "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", |       "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", | ||||||
| @@ -3446,9 +3732,26 @@ | |||||||
|       "version": "2.8.1", |       "version": "2.8.1", | ||||||
|       "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", |       "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", | ||||||
|       "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", |       "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", | ||||||
|       "dev": true, |  | ||||||
|       "license": "0BSD" |       "license": "0BSD" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/tweetnacl": { | ||||||
|  |       "version": "1.0.3", | ||||||
|  |       "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", | ||||||
|  |       "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", | ||||||
|  |       "license": "Unlicense" | ||||||
|  |     }, | ||||||
|  |     "node_modules/type-fest": { | ||||||
|  |       "version": "1.4.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", | ||||||
|  |       "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", | ||||||
|  |       "license": "(MIT OR CC0-1.0)", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=10" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "url": "https://github.com/sponsors/sindresorhus" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/type-is": { |     "node_modules/type-is": { | ||||||
|       "version": "1.6.18", |       "version": "1.6.18", | ||||||
|       "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", |       "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", | ||||||
| @@ -3475,12 +3778,33 @@ | |||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/undici": { | ||||||
|  |       "version": "7.15.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/undici/-/undici-7.15.0.tgz", | ||||||
|  |       "integrity": "sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=20.18.1" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/undici-types": { |     "node_modules/undici-types": { | ||||||
|       "version": "6.21.0", |       "version": "6.21.0", | ||||||
|       "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", |       "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", | ||||||
|       "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", |       "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", | ||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/unescape": { | ||||||
|  |       "version": "1.0.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/unescape/-/unescape-1.0.1.tgz", | ||||||
|  |       "integrity": "sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "extend-shallow": "^2.0.1" | ||||||
|  |       }, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=0.10.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/unpipe": { |     "node_modules/unpipe": { | ||||||
|       "version": "1.0.0", |       "version": "1.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", |       "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", | ||||||
| @@ -3490,6 +3814,36 @@ | |||||||
|         "node": ">= 0.8" |         "node": ">= 0.8" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/urllib": { | ||||||
|  |       "version": "4.8.2", | ||||||
|  |       "resolved": "https://registry.npmjs.org/urllib/-/urllib-4.8.2.tgz", | ||||||
|  |       "integrity": "sha512-V5oo9kzQfF9UQAC9KOVFmmmbYPJ9nksgO8HM89BZse96QcCyjrssPVxKzL/9sVPRC8D4Sd3nAdaMCXAZ3dqEYA==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "form-data": "^4.0.1", | ||||||
|  |         "formstream": "^1.5.1", | ||||||
|  |         "mime-types": "^2.1.35", | ||||||
|  |         "qs": "^6.12.1", | ||||||
|  |         "type-fest": "^4.20.1", | ||||||
|  |         "undici": "^7.1.1", | ||||||
|  |         "ylru": "^2.0.0" | ||||||
|  |       }, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">= 18.19.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/urllib/node_modules/type-fest": { | ||||||
|  |       "version": "4.41.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", | ||||||
|  |       "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", | ||||||
|  |       "license": "(MIT OR CC0-1.0)", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=16" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "url": "https://github.com/sponsors/sindresorhus" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/util": { |     "node_modules/util": { | ||||||
|       "version": "0.12.5", |       "version": "0.12.5", | ||||||
|       "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", |       "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", | ||||||
| @@ -3509,6 +3863,20 @@ | |||||||
|       "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", |       "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", | ||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/utility": { | ||||||
|  |       "version": "2.5.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/utility/-/utility-2.5.0.tgz", | ||||||
|  |       "integrity": "sha512-lDbOVde5UAKgtxrSyZNhqrTA7f7anba6DTqbsDWgUFk6PZlmr7djqPYw0FnL5a6TbJvRt38VmYqt07zVLzXG2A==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "escape-html": "^1.0.3", | ||||||
|  |         "unescape": "^1.0.1", | ||||||
|  |         "ylru": "^2.0.0" | ||||||
|  |       }, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">= 16.0.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/utils-merge": { |     "node_modules/utils-merge": { | ||||||
|       "version": "1.0.1", |       "version": "1.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", |       "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", | ||||||
| @@ -3742,6 +4110,15 @@ | |||||||
|         "node": ">=12" |         "node": ">=12" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/ylru": { | ||||||
|  |       "version": "2.0.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/ylru/-/ylru-2.0.0.tgz", | ||||||
|  |       "integrity": "sha512-T6hTrKcr9lKeUG0MQ/tO72D3UGptWVohgzpHG8ljU1jeBt2RCjcWxvsTPD8ZzUq1t1FvwROAw1kxg2euvg/THg==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">= 18.19.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/z-schema": { |     "node_modules/z-schema": { | ||||||
|       "version": "5.0.5", |       "version": "5.0.5", | ||||||
|       "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", |       "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", | ||||||
|   | |||||||
| @@ -9,10 +9,12 @@ | |||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@alicloud/dysmsapi20170525": "^4.1.2", |     "@alicloud/dysmsapi20170525": "^4.1.2", | ||||||
|     "@alicloud/openapi-client": "^0.4.15", |     "@alicloud/openapi-client": "^0.4.15", | ||||||
|  |     "alipay-sdk": "^4.14.0", | ||||||
|     "axios": "^1.11.0", |     "axios": "^1.11.0", | ||||||
|     "bcryptjs": "^2.4.3", |     "bcryptjs": "^2.4.3", | ||||||
|     "body-parser": "^1.20.2", |     "body-parser": "^1.20.2", | ||||||
|     "cors": "^2.8.5", |     "cors": "^2.8.5", | ||||||
|  |     "crypto": "^1.0.1", | ||||||
|     "dayjs": "^1.11.13", |     "dayjs": "^1.11.13", | ||||||
|     "dotenv": "^17.2.1", |     "dotenv": "^17.2.1", | ||||||
|     "express": "^4.18.2", |     "express": "^4.18.2", | ||||||
| @@ -25,10 +27,12 @@ | |||||||
|     "multer": "^1.4.5-lts.1", |     "multer": "^1.4.5-lts.1", | ||||||
|     "mysql2": "^3.14.3", |     "mysql2": "^3.14.3", | ||||||
|     "node-cron": "^4.2.1", |     "node-cron": "^4.2.1", | ||||||
|  |     "node-rsa": "^1.1.1", | ||||||
|     "qrcode": "^1.5.4", |     "qrcode": "^1.5.4", | ||||||
|     "swagger-jsdoc": "^6.2.8", |     "swagger-jsdoc": "^6.2.8", | ||||||
|     "swagger-ui-express": "^5.0.1", |     "swagger-ui-express": "^5.0.1", | ||||||
|     "winston": "^3.17.0" |     "winston": "^3.17.0", | ||||||
|  |     "xml2js": "^0.6.2" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "concurrently": "^8.2.2", |     "concurrently": "^8.2.2", | ||||||
|   | |||||||
							
								
								
									
										399
									
								
								routes/announcements.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										399
									
								
								routes/announcements.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,399 @@ | |||||||
|  | const express = require('express'); | ||||||
|  | const { getDB } = require('../database'); | ||||||
|  | const { auth, adminAuth } = require('../middleware/auth'); | ||||||
|  | const dayjs = require('dayjs'); | ||||||
|  |  | ||||||
|  | const router = express.Router(); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | router.get('/', auth, async (req, res) => { | ||||||
|  |   try { | ||||||
|  |     const db = getDB(); | ||||||
|  |     const { page = 1, limit = 10, status, type, keyword } = req.query; | ||||||
|  |     const offset = (page - 1) * limit; | ||||||
|  |      | ||||||
|  |     let whereClause = 'WHERE 1=1'; | ||||||
|  |     const params = []; | ||||||
|  |      | ||||||
|  |     if (status) { | ||||||
|  |       whereClause += ' AND status = ?'; | ||||||
|  |       params.push(status); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     if (type) { | ||||||
|  |       whereClause += ' AND type = ?'; | ||||||
|  |       params.push(type); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     if (keyword) { | ||||||
|  |       whereClause += ' AND (title LIKE ? OR content LIKE ?)'; | ||||||
|  |       params.push(`%${keyword}%`, `%${keyword}%`); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // 获取总数 | ||||||
|  |     const countQuery = `SELECT COUNT(*) as total FROM announcements ${whereClause}`; | ||||||
|  |     const [countResult] = await db.execute(countQuery, params); | ||||||
|  |     const total = countResult[0].total; | ||||||
|  |      | ||||||
|  |     // 获取公告列表(包含用户阅读状态) | ||||||
|  |     const limitValue = Math.max(1, Math.min(100, parseInt(limit))); | ||||||
|  |     const offsetValue = Math.max(0, parseInt(offset)); | ||||||
|  |      | ||||||
|  |     const query = ` | ||||||
|  |       SELECT a.*, u.username as creator_name, | ||||||
|  |              uar.is_read, | ||||||
|  |              uar.read_at, | ||||||
|  |              CASE  | ||||||
|  |                WHEN a.expire_time IS NOT NULL AND a.expire_time < NOW() THEN 1 | ||||||
|  |                ELSE 0 | ||||||
|  |              END as is_expired | ||||||
|  |       FROM announcements a  | ||||||
|  |       LEFT JOIN users u ON a.created_by = u.id  | ||||||
|  |       LEFT JOIN user_announcement_reads uar ON a.id = uar.announcement_id AND uar.user_id = ? | ||||||
|  |       ${whereClause}  | ||||||
|  |       ORDER BY a.is_pinned DESC, a.created_at DESC  | ||||||
|  |       LIMIT ${limitValue} OFFSET ${offsetValue} | ||||||
|  |     `; | ||||||
|  |      | ||||||
|  |     const [announcements] = await db.execute(query, [req.user.id, ...params]); | ||||||
|  |      | ||||||
|  |     // 自动将过期公告标记为已读 | ||||||
|  |     console.log(announcements); | ||||||
|  |      | ||||||
|  |     const expiredUnreadAnnouncements = announcements.filter(a => a.is_expired && !a.is_read); | ||||||
|  |     console.log(expiredUnreadAnnouncements); | ||||||
|  |      | ||||||
|  |     if (expiredUnreadAnnouncements.length > 0) { | ||||||
|  |       const expiredIds = expiredUnreadAnnouncements.map(a => a.id); | ||||||
|  |       await db.execute(` | ||||||
|  |         INSERT INTO user_announcement_reads (user_id, announcement_id, is_read, read_at) | ||||||
|  |         VALUES ${expiredIds.map(() => '(?, ?, TRUE, NOW())').join(', ')} | ||||||
|  |         ON DUPLICATE KEY UPDATE is_read = TRUE, read_at = NOW() | ||||||
|  |       `, expiredIds.flatMap(id => [req.user.id, id])); | ||||||
|  |        | ||||||
|  |       // 更新返回数据中的阅读状态 | ||||||
|  |       expiredUnreadAnnouncements.forEach(a => { | ||||||
|  |         a.is_read = true; | ||||||
|  |         a.read_at = new Date(); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     res.json({ | ||||||
|  |       success: true, | ||||||
|  |       data: { | ||||||
|  |         announcements, | ||||||
|  |         total, | ||||||
|  |         page: parseInt(page), | ||||||
|  |         limit: parseInt(limit), | ||||||
|  |         totalPages: Math.ceil(total / limit) | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('获取公告列表失败:', error); | ||||||
|  |     res.status(500).json({ success: false, message: '获取公告列表失败' }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | router.get('/:id', auth, async (req, res) => { | ||||||
|  |   try { | ||||||
|  |     const db = getDB(); | ||||||
|  |     const { id } = req.params; | ||||||
|  |      | ||||||
|  |     const query = ` | ||||||
|  |       SELECT a.*, u.username as creator_name, | ||||||
|  |              uar.is_read, | ||||||
|  |              uar.read_at, | ||||||
|  |              CASE  | ||||||
|  |                WHEN a.expire_time IS NOT NULL AND a.expire_time < NOW() THEN 1 | ||||||
|  |                ELSE 0 | ||||||
|  |              END as is_expired | ||||||
|  |       FROM announcements a  | ||||||
|  |       LEFT JOIN users u ON a.created_by = u.id  | ||||||
|  |       LEFT JOIN user_announcement_reads uar ON a.id = uar.announcement_id AND uar.user_id = ? | ||||||
|  |       WHERE a.id = ? | ||||||
|  |     `; | ||||||
|  |      | ||||||
|  |     const [result] = await db.execute(query, [req.user.id, id]); | ||||||
|  |      | ||||||
|  |     if (result.length === 0) { | ||||||
|  |       return res.status(404).json({ success: false, message: '公告不存在' }); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     const announcement = result[0]; | ||||||
|  |      | ||||||
|  |     // 如果公告未读或已过期但未标记为已读,则标记为已读 | ||||||
|  |     if (!announcement.is_read || (announcement.is_expired && !announcement.is_read)) { | ||||||
|  |       await db.execute(` | ||||||
|  |         INSERT INTO user_announcement_reads (user_id, announcement_id, is_read, read_at) | ||||||
|  |         VALUES (?, ?, TRUE, NOW()) | ||||||
|  |         ON DUPLICATE KEY UPDATE is_read = TRUE, read_at = NOW() | ||||||
|  |       `, [req.user.id, id]); | ||||||
|  |        | ||||||
|  |       announcement.is_read = true; | ||||||
|  |       announcement.read_at = new Date(); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     res.json({ success: true, data: announcement }); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('获取公告详情失败:', error); | ||||||
|  |     res.status(500).json({ success: false, message: '获取公告详情失败' }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | router.post('/', auth, adminAuth, async (req, res) => { | ||||||
|  |   try { | ||||||
|  |     const db = getDB(); | ||||||
|  |     const { | ||||||
|  |       title, | ||||||
|  |       content, | ||||||
|  |       type = 'system', | ||||||
|  |       priority = 'medium', | ||||||
|  |       status = 'draft', | ||||||
|  |       is_pinned = false, | ||||||
|  |       publish_time, | ||||||
|  |       expire_time | ||||||
|  |     } = req.body; | ||||||
|  |      | ||||||
|  |     if (!title || !content) { | ||||||
|  |       return res.status(400).json({ success: false, message: '标题和内容不能为空' }); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     const query = ` | ||||||
|  |       INSERT INTO announcements ( | ||||||
|  |         title, content, type, priority, status, is_pinned,  | ||||||
|  |         publish_time, expire_time, created_by, created_at, updated_at | ||||||
|  |       ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) | ||||||
|  |     `; | ||||||
|  |      | ||||||
|  |     const [result] = await db.execute(query, [ | ||||||
|  |       title, | ||||||
|  |       content, | ||||||
|  |       type, | ||||||
|  |       priority, | ||||||
|  |       status, | ||||||
|  |       is_pinned, | ||||||
|  |       publish_time || null, | ||||||
|  |       expire_time || null, | ||||||
|  |       req.user.id | ||||||
|  |     ]); | ||||||
|  |      | ||||||
|  |     res.status(201).json({ | ||||||
|  |       success: true, | ||||||
|  |       message: '公告创建成功', | ||||||
|  |       data: { id: result.insertId } | ||||||
|  |     }); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('创建公告失败:', error); | ||||||
|  |     res.status(500).json({ success: false, message: '创建公告失败' }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | router.put('/:id', auth, adminAuth, async (req, res) => { | ||||||
|  |   try { | ||||||
|  |     const db = getDB(); | ||||||
|  |     const { id } = req.params; | ||||||
|  |     let { | ||||||
|  |       title, | ||||||
|  |       content, | ||||||
|  |       type, | ||||||
|  |       priority, | ||||||
|  |       status, | ||||||
|  |       is_pinned, | ||||||
|  |       publish_time, | ||||||
|  |       expire_time | ||||||
|  |     } = req.body; | ||||||
|  |      | ||||||
|  |     // 检查公告是否存在 | ||||||
|  |     const [existing] = await db.execute('SELECT id FROM announcements WHERE id = ?', [id]); | ||||||
|  |     if (existing.length === 0) { | ||||||
|  |       return res.status(404).json({ success: false, message: '公告不存在' }); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     const updates = []; | ||||||
|  |     const params = []; | ||||||
|  |      | ||||||
|  |     if (title !== undefined) { | ||||||
|  |       updates.push('title = ?'); | ||||||
|  |       params.push(title); | ||||||
|  |     } | ||||||
|  |     if (content !== undefined) { | ||||||
|  |       updates.push('content = ?'); | ||||||
|  |       params.push(content); | ||||||
|  |     } | ||||||
|  |     if (type !== undefined) { | ||||||
|  |       updates.push('type = ?'); | ||||||
|  |       params.push(type); | ||||||
|  |     } | ||||||
|  |     if (priority !== undefined) { | ||||||
|  |       updates.push('priority = ?'); | ||||||
|  |       params.push(priority); | ||||||
|  |     } | ||||||
|  |     if (status !== undefined) { | ||||||
|  |       updates.push('status = ?'); | ||||||
|  |       params.push(status); | ||||||
|  |     } | ||||||
|  |     if (is_pinned !== undefined) { | ||||||
|  |       updates.push('is_pinned = ?'); | ||||||
|  |       params.push(is_pinned); | ||||||
|  |     } | ||||||
|  |     if (publish_time !== undefined) { | ||||||
|  |       updates.push('publish_time = ?'); | ||||||
|  |       publish_time = dayjs(publish_time).format('YYYY-MM-DD'); | ||||||
|  |       params.push(publish_time); | ||||||
|  |     } | ||||||
|  |     if (expire_time !== undefined) { | ||||||
|  |       updates.push('expire_time = ?'); | ||||||
|  |       params.push(expire_time); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     if (updates.length === 0) { | ||||||
|  |       return res.status(400).json({ success: false, message: '没有要更新的字段' }); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     updates.push('updated_at = NOW()'); | ||||||
|  |     params.push(id); | ||||||
|  |      | ||||||
|  |     const query = `UPDATE announcements SET ${updates.join(', ')} WHERE id = ?`; | ||||||
|  |     await db.execute(query, params); | ||||||
|  |      | ||||||
|  |     res.json({ success: true, message: '公告更新成功' }); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('更新公告失败:', error); | ||||||
|  |     res.status(500).json({ success: false, message: '更新公告失败' }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | router.delete('/:id', auth, adminAuth, async (req, res) => { | ||||||
|  |   try { | ||||||
|  |     const db = getDB(); | ||||||
|  |     const { id } = req.params; | ||||||
|  |      | ||||||
|  |     // 检查公告是否存在 | ||||||
|  |     const [existing] = await db.execute('SELECT id FROM announcements WHERE id = ?', [id]); | ||||||
|  |     if (existing.length === 0) { | ||||||
|  |       return res.status(404).json({ success: false, message: '公告不存在' }); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     await db.execute('DELETE FROM announcements WHERE id = ?', [id]); | ||||||
|  |      | ||||||
|  |     res.json({ success: true, message: '公告删除成功' }); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('删除公告失败:', error); | ||||||
|  |     res.status(500).json({ success: false, message: '删除公告失败' }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | router.get('/public/list', async (req, res) => { | ||||||
|  |   try { | ||||||
|  |     const db = getDB(); | ||||||
|  |     const { limit = 5 } = req.query; | ||||||
|  |     const limitValue = Math.max(1, Math.min(50, parseInt(limit))); | ||||||
|  |      | ||||||
|  |     const query = ` | ||||||
|  |       SELECT id, title, content, type, priority, publish_time, created_at | ||||||
|  |       FROM announcements  | ||||||
|  |       WHERE status = 'published'  | ||||||
|  |         AND (expire_time IS NULL OR expire_time > NOW()) | ||||||
|  |         AND (publish_time IS NULL OR publish_time <= NOW()) | ||||||
|  |       ORDER BY is_pinned DESC, created_at DESC  | ||||||
|  |       LIMIT ${limitValue} | ||||||
|  |     `; | ||||||
|  |      | ||||||
|  |     const [announcements] = await db.execute(query, []); | ||||||
|  |      | ||||||
|  |     res.json({ success: true, data: announcements }); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('获取公开公告失败:', error); | ||||||
|  |     res.status(500).json({ success: false, message: '获取公开公告失败' }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | // 标记公告为已读 | ||||||
|  | router.post('/:id/read', auth, async (req, res) => { | ||||||
|  |   try { | ||||||
|  |     const db = getDB(); | ||||||
|  |     const { id } = req.params; | ||||||
|  |      | ||||||
|  |     // 检查公告是否存在 | ||||||
|  |     const [existing] = await db.execute('SELECT id FROM announcements WHERE id = ?', [id]); | ||||||
|  |     if (existing.length === 0) { | ||||||
|  |       return res.status(404).json({ success: false, message: '公告不存在' }); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // 标记为已读 | ||||||
|  |     await db.execute(` | ||||||
|  |       INSERT INTO user_announcement_reads (user_id, announcement_id, is_read, read_at) | ||||||
|  |       VALUES (?, ?, TRUE, NOW()) | ||||||
|  |       ON DUPLICATE KEY UPDATE is_read = TRUE, read_at = NOW() | ||||||
|  |     `, [req.user.id, id]); | ||||||
|  |      | ||||||
|  |     res.json({ success: true, message: '已标记为已读' }); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('标记公告已读失败:', error); | ||||||
|  |     res.status(500).json({ success: false, message: '标记公告已读失败' }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | // 获取用户未读公告数量 | ||||||
|  | router.get('/unread/count', auth, async (req, res) => { | ||||||
|  |   try { | ||||||
|  |     const db = getDB(); | ||||||
|  |      | ||||||
|  |     const query = ` | ||||||
|  |       SELECT COUNT(*) as unread_count | ||||||
|  |       FROM announcements a | ||||||
|  |       LEFT JOIN user_announcement_reads uar ON a.id = uar.announcement_id AND uar.user_id = ? | ||||||
|  |       WHERE a.status = 'published'  | ||||||
|  |         AND (a.publish_time IS NULL OR a.publish_time <= NOW()) | ||||||
|  |         AND (a.expire_time IS NULL OR a.expire_time > NOW()) | ||||||
|  |         AND (uar.is_read IS NULL OR uar.is_read = FALSE) | ||||||
|  |     `; | ||||||
|  |      | ||||||
|  |     const [result] = await db.execute(query, [req.user.id]); | ||||||
|  |      | ||||||
|  |     res.json({  | ||||||
|  |       success: true,  | ||||||
|  |       data: {  | ||||||
|  |         unread_count: result[0].unread_count  | ||||||
|  |       }  | ||||||
|  |     }); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('获取未读公告数量失败:', error); | ||||||
|  |     res.status(500).json({ success: false, message: '获取未读公告数量失败' }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | // 批量标记公告为已读 | ||||||
|  | router.post('/batch/read', auth, async (req, res) => { | ||||||
|  |   try { | ||||||
|  |     const db = getDB(); | ||||||
|  |     const { announcement_ids } = req.body; | ||||||
|  |      | ||||||
|  |     if (!announcement_ids || !Array.isArray(announcement_ids) || announcement_ids.length === 0) { | ||||||
|  |       return res.status(400).json({ success: false, message: '请提供有效的公告ID列表' }); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // 批量标记为已读 | ||||||
|  |     const values = announcement_ids.map(() => '(?, ?, TRUE, NOW())').join(', '); | ||||||
|  |     const params = announcement_ids.flatMap(id => [req.user.id, id]); | ||||||
|  |      | ||||||
|  |     await db.execute(` | ||||||
|  |       INSERT INTO user_announcement_reads (user_id, announcement_id, is_read, read_at) | ||||||
|  |       VALUES ${values} | ||||||
|  |       ON DUPLICATE KEY UPDATE is_read = TRUE, read_at = NOW() | ||||||
|  |     `, params); | ||||||
|  |      | ||||||
|  |     res.json({ success: true, message: '批量标记已读成功' }); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('批量标记公告已读失败:', error); | ||||||
|  |     res.status(500).json({ success: false, message: '批量标记公告已读失败' }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | module.exports = router; | ||||||
| @@ -127,7 +127,6 @@ router.post('/register', async (req, res) => { | |||||||
|       username, |       username, | ||||||
|       phone, |       phone, | ||||||
|       password, |       password, | ||||||
|       registrationCode, |  | ||||||
|       city, |       city, | ||||||
|       district_id: district, |       district_id: district, | ||||||
|       captchaId, |       captchaId, | ||||||
| @@ -136,8 +135,8 @@ router.post('/register', async (req, res) => { | |||||||
|       role = 'user' |       role = 'user' | ||||||
|     } = req.body; |     } = req.body; | ||||||
|  |  | ||||||
|     if (!username || !phone || !password || !registrationCode || !city || !district) { |     if (!username || !phone || !password || !city || !district) { | ||||||
|       return res.status(400).json({ success: false, message: '用户名、手机号、密码、激活码、城市和区域不能为空' }); |       return res.status(400).json({ success: false, message: '用户名、手机号、密码、城市和区域不能为空' }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (!captchaId || !captchaText) { |     if (!captchaId || !captchaText) { | ||||||
| @@ -163,44 +162,31 @@ router.post('/register', async (req, res) => { | |||||||
|       return res.status(400).json({ success: false, message: '手机号格式不正确' }); |       return res.status(400).json({ success: false, message: '手机号格式不正确' }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // 验证激活码 |  | ||||||
|     const [registrationCodes] = await db.execute( |  | ||||||
|       'SELECT id, is_used, expires_at, agent_id FROM registration_codes WHERE code = ?', |  | ||||||
|       [registrationCode] |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     if (registrationCodes.length === 0) { |  | ||||||
|       return res.status(400).json({ success: false, message: '激活码不存在' }); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     const regCode = registrationCodes[0]; |  | ||||||
|      |  | ||||||
|     // 检查激活码是否已使用 |  | ||||||
|     if (regCode.is_used) { |  | ||||||
|       return res.status(400).json({ success: false, message: '激活码已被使用' }); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // 检查激活码是否过期 |  | ||||||
|     if (new Date() > new Date(regCode.expires_at)) { |  | ||||||
|       return res.status(400).json({ success: false, message: '激活码已过期' }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // 检查用户是否已存在 |     // 检查用户是否已存在 | ||||||
|     const [existingUsers] = await db.execute( |     const [existingUsers] = await db.execute( | ||||||
|       'SELECT id FROM users WHERE username = ? OR phone = ?', |       'SELECT id, payment_status FROM users WHERE username = ? OR phone = ?', | ||||||
|       [username, phone] |       [username, phone] | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     if (existingUsers.length > 0) { |     if (existingUsers.length > 0) { | ||||||
|  |       const existingUser = existingUsers[0]; | ||||||
|  |       // 如果用户存在但未支付,允许重新注册(覆盖原用户信息) | ||||||
|  |       if (existingUser.payment_status === 'unpaid') { | ||||||
|  |         // 删除未支付的用户记录 | ||||||
|  |         await db.execute('DELETE FROM users WHERE id = ?', [existingUser.id]); | ||||||
|  |       } else { | ||||||
|         return res.status(400).json({ success: false, message: '用户名或手机号已存在' }); |         return res.status(400).json({ success: false, message: '用户名或手机号已存在' }); | ||||||
|       } |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     // 加密密码 |     // 加密密码 | ||||||
|     const hashedPassword = await bcrypt.hash(password, 10); |     const hashedPassword = await bcrypt.hash(password, 10); | ||||||
|  |  | ||||||
|     // 创建用户(待审核状态,可以进入系统但匹配需审核) |     // 创建用户(初始状态为未支付) | ||||||
|     const [result] = await db.execute( |     const [result] = await db.execute( | ||||||
|       'INSERT INTO users (username, phone, password, role, points, audit_status, city, district_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', |       'INSERT INTO users (username, phone, password, role, points, audit_status, city, district_id, payment_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, "unpaid")', | ||||||
|       [username, phone, hashedPassword, role, 0, 'pending', city, district] |       [username, phone, hashedPassword, role, 0, 'pending', city, district] | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
| @@ -208,28 +194,9 @@ router.post('/register', async (req, res) => { | |||||||
|  |  | ||||||
|     // 用户余额已在创建用户时设置为默认值0.00,无需额外操作 |     // 用户余额已在创建用户时设置为默认值0.00,无需额外操作 | ||||||
|  |  | ||||||
|     // 标记激活码为已使用 |  | ||||||
|     await db.execute( |  | ||||||
|       'UPDATE registration_codes SET is_used = TRUE, used_at = NOW(), used_by_user_id = ? WHERE id = ?', |  | ||||||
|       [userId, regCode.id] |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     // 如果是代理邀请码,建立代理关系 |  | ||||||
|     if (regCode.agent_id) { |  | ||||||
|       // 验证agent_id是否存在于regional_agents表中 |  | ||||||
|       const [agentExists] = await db.execute( |  | ||||||
|         'SELECT id FROM regional_agents WHERE id = ?', |  | ||||||
|         [regCode.agent_id] |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       if (agentExists.length > 0) { |     // 根据地区自动关联代理 | ||||||
|         await db.execute( |  | ||||||
|           'INSERT INTO agent_merchants (agent_id, merchant_id, created_at) VALUES (?, ?, NOW())', |  | ||||||
|           [regCode.agent_id, userId] |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|     } else { |  | ||||||
|       // 如果不是代理邀请码,根据地区自动关联代理 |  | ||||||
|     const [agents] = await db.execute( |     const [agents] = await db.execute( | ||||||
|       'SELECT ra.id FROM users u INNER JOIN regional_agents ra ON u.id = ra.user_id WHERE ra.region_id = ? AND ra.status = "active" ORDER BY ra.created_at ASC LIMIT 1', |       'SELECT ra.id FROM users u INNER JOIN regional_agents ra ON u.id = ra.user_id WHERE ra.region_id = ? AND ra.status = "active" ORDER BY ra.created_at ASC LIMIT 1', | ||||||
|       [district] |       [district] | ||||||
| @@ -241,11 +208,10 @@ router.post('/register', async (req, res) => { | |||||||
|         [agents[0].id, userId] |         [agents[0].id, userId] | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|     } |  | ||||||
|  |  | ||||||
|     await db.query('COMMIT'); |     await db.query('COMMIT'); | ||||||
|  |  | ||||||
|     // 生成JWT token |     // 生成JWT token(用于支付流程) | ||||||
|     const token = jwt.sign( |     const token = jwt.sign( | ||||||
|       { userId: userId, username, role }, |       { userId: userId, username, role }, | ||||||
|       JWT_SECRET, |       JWT_SECRET, | ||||||
| @@ -254,7 +220,7 @@ router.post('/register', async (req, res) => { | |||||||
|  |  | ||||||
|     res.status(201).json({ |     res.status(201).json({ | ||||||
|       success: true, |       success: true, | ||||||
|       message: '注册成功', |       message: '用户信息创建成功,请完成支付以激活账户', | ||||||
|       token, |       token, | ||||||
|       user: { |       user: { | ||||||
|         id: userId, |         id: userId, | ||||||
| @@ -264,8 +230,10 @@ router.post('/register', async (req, res) => { | |||||||
|         points: 0, |         points: 0, | ||||||
|         audit_status: 'pending', |         audit_status: 'pending', | ||||||
|         city, |         city, | ||||||
|         district |         district, | ||||||
|       } |         paymentStatus: 'unpaid' | ||||||
|  |       }, | ||||||
|  |       needPayment: true | ||||||
|     }); |     }); | ||||||
|   } catch (error) { |   } catch (error) { | ||||||
|     try { |     try { | ||||||
| @@ -379,7 +347,7 @@ router.post('/login', async (req, res) => { | |||||||
|  |  | ||||||
|     // 注意:验证码已在前端通过 /captcha/verify 接口验证过,这里不再重复验证 |     // 注意:验证码已在前端通过 /captcha/verify 接口验证过,这里不再重复验证 | ||||||
|  |  | ||||||
|     // 查找用户 |     // 查找用户(包含支付状态) | ||||||
|     console.log('登录尝试 - 用户名:', username); |     console.log('登录尝试 - 用户名:', username); | ||||||
|     const [users] = await db.execute( |     const [users] = await db.execute( | ||||||
|       'SELECT * FROM users WHERE username = ?', |       'SELECT * FROM users WHERE username = ?', | ||||||
| @@ -405,6 +373,22 @@ router.post('/login', async (req, res) => { | |||||||
|       return res.status(401).json({ success: false, message: '用户名或密码错误' }); |       return res.status(401).json({ success: false, message: '用户名或密码错误' }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     // 检查支付状态(管理员除外) | ||||||
|  |     if (user.role !== 'admin' && user.payment_status === 'unpaid') { | ||||||
|  |       const token = jwt.sign( | ||||||
|  |         { userId: user.id, username: user.username, role: user.role }, | ||||||
|  |         JWT_SECRET, | ||||||
|  |         { expiresIn: '5m' } | ||||||
|  |       ); | ||||||
|  |       return res.status(200).json({ | ||||||
|  |         success: false, | ||||||
|  |         message: '您的账户尚未激活,请完成支付后再登录', | ||||||
|  |         needPayment: true, | ||||||
|  |         user: user[0], | ||||||
|  |         token | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     // 检查用户审核状态(管理员除外,只阻止被拒绝的用户) |     // 检查用户审核状态(管理员除外,只阻止被拒绝的用户) | ||||||
|     if (user.role !== 'admin' && user.audit_status === 'rejected') { |     if (user.role !== 'admin' && user.audit_status === 'rejected') { | ||||||
|       return res.status(403).json({ success: false, message: '您的账户审核未通过,请联系管理员' }); |       return res.status(403).json({ success: false, message: '您的账户审核未通过,请联系管理员' }); | ||||||
| @@ -427,7 +411,8 @@ router.post('/login', async (req, res) => { | |||||||
|         username: user.username, |         username: user.username, | ||||||
|         role: user.role, |         role: user.role, | ||||||
|         avatar: user.avatar, |         avatar: user.avatar, | ||||||
|         points: user.points |         points: user.points, | ||||||
|  |         payment_status: user.payment_status | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|   } catch (error) { |   } catch (error) { | ||||||
|   | |||||||
							
								
								
									
										190
									
								
								routes/orders.js
									
									
									
									
									
								
							
							
						
						
									
										190
									
								
								routes/orders.js
									
									
									
									
									
								
							| @@ -114,9 +114,11 @@ router.get('/', auth, async (req, res) => { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       // 处理地址信息 |       // 处理地址信息 | ||||||
|  |       console.log(order.address,'order.address'); | ||||||
|  |        | ||||||
|       if (order.address) { |       if (order.address) { | ||||||
|         try { |         try { | ||||||
|           order.address = JSON.parse(order.address); |           order.address = order.address; | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|           order.address = null; |           order.address = null; | ||||||
|         } |         } | ||||||
| @@ -138,76 +140,7 @@ router.get('/', auth, async (req, res) => { | |||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     /** |      | ||||||
|      * @swagger |  | ||||||
|      * /api/orders/confirm: |  | ||||||
|      *   post: |  | ||||||
|      *     summary: 确认下单 |  | ||||||
|      *     tags: [Orders] |  | ||||||
|      *     security: |  | ||||||
|      *       - bearerAuth: [] |  | ||||||
|      *     requestBody: |  | ||||||
|      *       required: true |  | ||||||
|      *       content: |  | ||||||
|      *         application/json: |  | ||||||
|      *           schema: |  | ||||||
|      *             type: object |  | ||||||
|      *             required: |  | ||||||
|      *               - pre_order_id |  | ||||||
|      *               - address |  | ||||||
|      *             properties: |  | ||||||
|      *               pre_order_id: |  | ||||||
|      *                 type: integer |  | ||||||
|      *                 description: 预订单ID |  | ||||||
|      *               address: |  | ||||||
|      *                 type: object |  | ||||||
|      *                 properties: |  | ||||||
|      *                   recipient_name: |  | ||||||
|      *                     type: string |  | ||||||
|      *                     description: 收货人姓名 |  | ||||||
|      *                   phone: |  | ||||||
|      *                     type: string |  | ||||||
|      *                     description: 收货人电话 |  | ||||||
|      *                   province: |  | ||||||
|      *                     type: string |  | ||||||
|      *                     description: 省份 |  | ||||||
|      *                   city: |  | ||||||
|      *                     type: string |  | ||||||
|      *                     description: 城市 |  | ||||||
|      *                   district: |  | ||||||
|      *                     type: string |  | ||||||
|      *                     description: 区县 |  | ||||||
|      *                   detail_address: |  | ||||||
|      *                     type: string |  | ||||||
|      *                     description: 详细地址 |  | ||||||
|      *     responses: |  | ||||||
|      *       200: |  | ||||||
|      *         description: 确认下单成功 |  | ||||||
|      *         content: |  | ||||||
|      *           application/json: |  | ||||||
|      *             schema: |  | ||||||
|      *               type: object |  | ||||||
|      *               properties: |  | ||||||
|      *                 success: |  | ||||||
|      *                   type: boolean |  | ||||||
|      *                 message: |  | ||||||
|      *                   type: string |  | ||||||
|      *                 data: |  | ||||||
|      *                   type: object |  | ||||||
|      *                   properties: |  | ||||||
|      *                     order_id: |  | ||||||
|      *                       type: integer |  | ||||||
|      *                     order_no: |  | ||||||
|      *                       type: string |  | ||||||
|      *       400: |  | ||||||
|      *         description: 请求参数错误 |  | ||||||
|      *       401: |  | ||||||
|      *         description: 未授权 |  | ||||||
|      *       404: |  | ||||||
|      *         description: 预订单不存在 |  | ||||||
|      *       500: |  | ||||||
|      *         description: 服务器错误 |  | ||||||
|      */ |  | ||||||
|     router.post('/confirm', auth, async (req, res) => { |     router.post('/confirm', auth, async (req, res) => { | ||||||
|       const connection = await getDB().getConnection(); |       const connection = await getDB().getConnection(); | ||||||
|  |  | ||||||
| @@ -331,76 +264,7 @@ router.get('/', auth, async (req, res) => { | |||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     /** |      | ||||||
|      * @swagger |  | ||||||
|      * /api/orders/pre-order/{id}: |  | ||||||
|      *   get: |  | ||||||
|      *     summary: 获取预订单详情 |  | ||||||
|      *     tags: [Orders] |  | ||||||
|      *     security: |  | ||||||
|      *       - bearerAuth: [] |  | ||||||
|      *     parameters: |  | ||||||
|      *       - in: path |  | ||||||
|      *         name: id |  | ||||||
|      *         required: true |  | ||||||
|      *         schema: |  | ||||||
|      *           type: integer |  | ||||||
|      *         description: 预订单ID |  | ||||||
|      *     responses: |  | ||||||
|      *       200: |  | ||||||
|      *         description: 获取预订单详情成功 |  | ||||||
|      *         content: |  | ||||||
|      *           application/json: |  | ||||||
|      *             schema: |  | ||||||
|      *               type: object |  | ||||||
|      *               properties: |  | ||||||
|      *                 success: |  | ||||||
|      *                   type: boolean |  | ||||||
|      *                 data: |  | ||||||
|      *                   type: object |  | ||||||
|      *                   properties: |  | ||||||
|      *                     id: |  | ||||||
|      *                       type: integer |  | ||||||
|      *                     order_no: |  | ||||||
|      *                       type: string |  | ||||||
|      *                     total_amount: |  | ||||||
|      *                       type: integer |  | ||||||
|      *                     total_points: |  | ||||||
|      *                       type: integer |  | ||||||
|      *                     total_rongdou: |  | ||||||
|      *                       type: integer |  | ||||||
|      *                     status: |  | ||||||
|      *                       type: string |  | ||||||
|      *                     created_at: |  | ||||||
|      *                       type: string |  | ||||||
|      *                     items: |  | ||||||
|      *                       type: array |  | ||||||
|      *                       items: |  | ||||||
|      *                         type: object |  | ||||||
|      *                         properties: |  | ||||||
|      *                           id: |  | ||||||
|      *                             type: integer |  | ||||||
|      *                           product_id: |  | ||||||
|      *                             type: integer |  | ||||||
|      *                           product_name: |  | ||||||
|      *                             type: string |  | ||||||
|      *                           quantity: |  | ||||||
|      *                             type: integer |  | ||||||
|      *                           price: |  | ||||||
|      *                             type: integer |  | ||||||
|      *                           points_price: |  | ||||||
|      *                             type: integer |  | ||||||
|      *                           rongdou_price: |  | ||||||
|      *                             type: integer |  | ||||||
|      *                           spec_info: |  | ||||||
|      *                             type: object |  | ||||||
|      *       401: |  | ||||||
|      *         description: 未授权 |  | ||||||
|      *       404: |  | ||||||
|      *         description: 预订单不存在 |  | ||||||
|      *       500: |  | ||||||
|      *         description: 服务器错误 |  | ||||||
|      */ |  | ||||||
|     router.get('/pre-order/:id', auth, async (req, res) => { |     router.get('/pre-order/:id', auth, async (req, res) => { | ||||||
|       try { |       try { | ||||||
|         const preOrderId = req.params.id; |         const preOrderId = req.params.id; | ||||||
| @@ -462,43 +326,7 @@ router.get('/', auth, async (req, res) => { | |||||||
|   } |   } | ||||||
| }); | }); | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @swagger |  | ||||||
|  * /api/orders/{id}: |  | ||||||
|  *   get: |  | ||||||
|  *     summary: 获取单个订单详情 |  | ||||||
|  *     tags: [Orders] |  | ||||||
|  *     security: |  | ||||||
|  *       - bearerAuth: [] |  | ||||||
|  *     parameters: |  | ||||||
|  *       - in: path |  | ||||||
|  *         name: id |  | ||||||
|  *         required: true |  | ||||||
|  *         schema: |  | ||||||
|  *           type: integer |  | ||||||
|  *         description: 订单ID |  | ||||||
|  *     responses: |  | ||||||
|  *       200: |  | ||||||
|  *         description: 成功获取订单详情 |  | ||||||
|  *         content: |  | ||||||
|  *           application/json: |  | ||||||
|  *             schema: |  | ||||||
|  *               type: object |  | ||||||
|  *               properties: |  | ||||||
|  *                 success: |  | ||||||
|  *                   type: boolean |  | ||||||
|  *                 data: |  | ||||||
|  *                   type: object |  | ||||||
|  *                   properties: |  | ||||||
|  *                     order: |  | ||||||
|  *                       $ref: '#/components/schemas/Order' |  | ||||||
|  *       401: |  | ||||||
|  *         description: 未授权 |  | ||||||
|  *       404: |  | ||||||
|  *         description: 订单不存在 |  | ||||||
|  *       500: |  | ||||||
|  *         description: 服务器错误 |  | ||||||
|  */ |  | ||||||
| router.get('/:id', auth, async (req, res) => { | router.get('/:id', auth, async (req, res) => { | ||||||
|   try { |   try { | ||||||
|     const { id } = req.params; |     const { id } = req.params; | ||||||
| @@ -557,9 +385,11 @@ router.get('/:id', auth, async (req, res) => { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     // 处理地址信息 |     // 处理地址信息 | ||||||
|  |     console.log(order.address,'order.address'); | ||||||
|  |      | ||||||
|     if (order.address) { |     if (order.address) { | ||||||
|       try { |       try { | ||||||
|         order.address = JSON.parse(order.address); |         order.address = order.address; | ||||||
|       } catch (e) { |       } catch (e) { | ||||||
|         order.address = null; |         order.address = null; | ||||||
|       } |       } | ||||||
| @@ -1340,7 +1170,7 @@ router.post('/confirm-payment', auth, async (req, res) => { | |||||||
|       } |       } | ||||||
|       pointsToDeduct = 0; |       pointsToDeduct = 0; | ||||||
|       rongdouToDeduct = totalRongdouNeeded; |       rongdouToDeduct = totalRongdouNeeded; | ||||||
|     } else if (isComboPayment) { |     } else if (hasPoints && hasRongdou) { | ||||||
|       // 组合支付:先扣积分,不足部分用融豆 |       // 组合支付:先扣积分,不足部分用融豆 | ||||||
|       const availablePointsInRongdou = Math.floor(user.points / 10000); // 积分可转换的融豆数 |       const availablePointsInRongdou = Math.floor(user.points / 10000); // 积分可转换的融豆数 | ||||||
|        |        | ||||||
|   | |||||||
							
								
								
									
										400
									
								
								routes/payment.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										400
									
								
								routes/payment.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,400 @@ | |||||||
|  | const express = require('express'); | ||||||
|  | const router = express.Router(); | ||||||
|  | const WechatPayService = require('../services/wechatPayService'); | ||||||
|  | const AlipayService = require('../services/alipayservice'); | ||||||
|  | const { getDB } = require('../database'); | ||||||
|  | const { auth, paymentAuth } = require('../middleware/auth'); | ||||||
|  |  | ||||||
|  | // 创建支付服务实例 | ||||||
|  | const wechatPayService = new WechatPayService(); | ||||||
|  | const alipayService = new AlipayService(); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取支持的支付方式 | ||||||
|  |  * GET /api/payment/methods | ||||||
|  |  */ | ||||||
|  | router.get('/methods', (req, res) => { | ||||||
|  |   res.json({ | ||||||
|  |     success: true, | ||||||
|  |     data: { | ||||||
|  |       methods: [ | ||||||
|  |         { | ||||||
|  |           code: 'wechat_h5', | ||||||
|  |           name: '微信支付', | ||||||
|  |           description: '微信H5支付', | ||||||
|  |           icon: 'wechat', | ||||||
|  |           enabled: true | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           code: 'alipay_wap', | ||||||
|  |           name: '支付宝支付', | ||||||
|  |           description: '支付宝手机网站支付', | ||||||
|  |           icon: 'alipay', | ||||||
|  |           enabled: true | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 创建统一支付订单 | ||||||
|  |  * POST /api/payment/create-order | ||||||
|  |  */ | ||||||
|  | router.post('/create-order', paymentAuth, async (req, res) => { | ||||||
|  |   try { | ||||||
|  |     const { paymentMethod } = req.body; | ||||||
|  |     const userId = req.user.id; | ||||||
|  |     const username = req.user.username; | ||||||
|  |     const phone = req.user.phone; | ||||||
|  |      | ||||||
|  |     // 验证支付方式 | ||||||
|  |     if (!paymentMethod || !['wechat_h5', 'alipay_wap'].includes(paymentMethod)) { | ||||||
|  |       return res.status(400).json({ | ||||||
|  |         success: false, | ||||||
|  |         message: '不支持的支付方式' | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // 检查用户是否已经支付过 | ||||||
|  |     const db = getDB(); | ||||||
|  |     const [existingOrders] = await db.execute( | ||||||
|  |       'SELECT id FROM payment_orders WHERE user_id = ? AND status = "paid"', | ||||||
|  |       [userId] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     if (existingOrders.length > 0) { | ||||||
|  |       return res.status(400).json({ | ||||||
|  |         success: false, | ||||||
|  |         message: '用户已完成支付,无需重复支付' | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let result; | ||||||
|  |      | ||||||
|  |     // 获取客户端IP | ||||||
|  |     const clientIp = req.headers['x-forwarded-for'] ||  | ||||||
|  |                     req.headers['x-real-ip'] ||  | ||||||
|  |                     req.connection.remoteAddress ||  | ||||||
|  |                     req.socket.remoteAddress || | ||||||
|  |                     (req.connection.socket ? req.connection.socket.remoteAddress : null) || | ||||||
|  |                     '127.0.0.1'; | ||||||
|  |      | ||||||
|  |     // 根据支付方式创建订单 | ||||||
|  |     if (paymentMethod === 'wechat_h5') { | ||||||
|  |       // 创建微信支付订单 | ||||||
|  |       result = await wechatPayService.createRegistrationPayOrder({ | ||||||
|  |         userId, | ||||||
|  |         username, | ||||||
|  |         phone, | ||||||
|  |         clientIp | ||||||
|  |       }); | ||||||
|  |     } else if (paymentMethod === 'alipay_wap') { | ||||||
|  |       // 创建支付宝支付订单 | ||||||
|  |       result = await alipayService.createRegistrationPayOrder({ | ||||||
|  |         userId, | ||||||
|  |         username, | ||||||
|  |         phone, | ||||||
|  |         clientIp | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (result && result.success) { | ||||||
|  |       res.json({ | ||||||
|  |         success: true, | ||||||
|  |         data: { | ||||||
|  |           outTradeNo: result.data.outTradeNo, | ||||||
|  |           payUrl: result.data.h5Url || result.data.payUrl, | ||||||
|  |           paymentType: result.data.paymentType, | ||||||
|  |           paymentMethod | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     } else { | ||||||
|  |       res.status(500).json({ | ||||||
|  |         success: false, | ||||||
|  |         message: '创建支付订单失败' | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('创建统一支付订单异常:', error); | ||||||
|  |     res.status(500).json({ | ||||||
|  |       success: false, | ||||||
|  |       message: error.message || '服务器内部错误' | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 查询支付状态 | ||||||
|  |  * GET /api/payment/query-status/:outTradeNo | ||||||
|  |  */ | ||||||
|  | router.get('/query-status/:outTradeNo', paymentAuth, async (req, res) => { | ||||||
|  |   try { | ||||||
|  |     const { outTradeNo } = req.params; | ||||||
|  |     const userId = req.user.id; | ||||||
|  |  | ||||||
|  |     // 验证订单是否属于当前用户 | ||||||
|  |     const db = getDB(); | ||||||
|  |     const [orders] = await db.execute( | ||||||
|  |       'SELECT id FROM payment_orders WHERE out_trade_no = ? AND user_id = ?', | ||||||
|  |       [outTradeNo, userId] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     if (orders.length === 0) { | ||||||
|  |       return res.status(404).json({ | ||||||
|  |         success: false, | ||||||
|  |         message: '订单不存在或无权限访问' | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // 获取订单详细信息,包括trade_type | ||||||
|  |     const [orderDetails] = await db.execute( | ||||||
|  |       'SELECT id, trade_type FROM payment_orders WHERE id = ?', | ||||||
|  |       [orders[0].id] | ||||||
|  |     ); | ||||||
|  |      | ||||||
|  |     if (orderDetails.length === 0) { | ||||||
|  |       return res.status(404).json({ | ||||||
|  |         success: false, | ||||||
|  |         message: '订单详情不存在' | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     let result; | ||||||
|  |     const tradeType = orderDetails[0].trade_type; | ||||||
|  |      | ||||||
|  |     // 根据交易类型查询支付状态 | ||||||
|  |     if (tradeType === 'WECHAT_H5') { | ||||||
|  |       // 查询微信支付状态 | ||||||
|  |       result = await wechatPayService.queryPaymentStatus(outTradeNo); | ||||||
|  |     } else if (tradeType === 'ALIPAY_WAP') { | ||||||
|  |       // 查询支付宝支付状态 | ||||||
|  |       result = await alipayService.queryPaymentStatus(outTradeNo); | ||||||
|  |     } else { | ||||||
|  |       return res.status(400).json({ | ||||||
|  |         success: false, | ||||||
|  |         message: '不支持的支付方式' | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     res.json(result); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('查询支付状态失败:', error); | ||||||
|  |     res.status(500).json({ | ||||||
|  |       success: false, | ||||||
|  |       message: error.message || '查询支付状态失败' | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取用户支付记录 | ||||||
|  |  * GET /api/payment/orders | ||||||
|  |  */ | ||||||
|  | router.get('/orders', paymentAuth, async (req, res) => { | ||||||
|  |   try { | ||||||
|  |     const userId = req.user.id; | ||||||
|  |     const { page = 1, limit = 10, status } = req.query; | ||||||
|  |      | ||||||
|  |     const offset = (page - 1) * limit; | ||||||
|  |     const db = getDB(); | ||||||
|  |      | ||||||
|  |     let whereClause = 'WHERE user_id = ?'; | ||||||
|  |     let params = [userId]; | ||||||
|  |      | ||||||
|  |     if (status) { | ||||||
|  |       whereClause += ' AND status = ?'; | ||||||
|  |       params.push(status); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // 查询订单列表 | ||||||
|  |     const [orders] = await db.execute( | ||||||
|  |       `SELECT id, out_trade_no, transaction_id, total_fee, body, trade_type,  | ||||||
|  |               status, paid_at, created_at  | ||||||
|  |        FROM payment_orders  | ||||||
|  |        ${whereClause}  | ||||||
|  |        ORDER BY created_at DESC  | ||||||
|  |        LIMIT ? OFFSET ?`, | ||||||
|  |       [...params, parseInt(limit), parseInt(offset)] | ||||||
|  |     ); | ||||||
|  |      | ||||||
|  |     // 查询总数 | ||||||
|  |     const [countResult] = await db.execute( | ||||||
|  |       `SELECT COUNT(*) as total FROM payment_orders ${whereClause}`, | ||||||
|  |       params | ||||||
|  |     ); | ||||||
|  |      | ||||||
|  |     const total = countResult[0].total; | ||||||
|  |      | ||||||
|  |     res.json({ | ||||||
|  |       success: true, | ||||||
|  |       data: { | ||||||
|  |         orders: orders.map(order => ({ | ||||||
|  |           ...order, | ||||||
|  |           total_fee: order.total_fee / 100, // 转换为元 | ||||||
|  |           payment_method_name: order.trade_type && order.trade_type.startsWith('ALIPAY') ? '支付宝支付' : '微信支付' | ||||||
|  |         })), | ||||||
|  |         pagination: { | ||||||
|  |           page: parseInt(page), | ||||||
|  |           limit: parseInt(limit), | ||||||
|  |           total, | ||||||
|  |           pages: Math.ceil(total / limit) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('获取支付记录失败:', error); | ||||||
|  |     res.status(500).json({ | ||||||
|  |       success: false, | ||||||
|  |       message: '获取支付记录失败' | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 检查用户支付状态 | ||||||
|  |  * GET /api/payment/check-status | ||||||
|  |  */ | ||||||
|  | router.get('/check-status', auth, async (req, res) => { | ||||||
|  |   try { | ||||||
|  |     const userId = req.user.id; | ||||||
|  |     const db = getDB(); | ||||||
|  |  | ||||||
|  |     // 查询用户支付状态 | ||||||
|  |     const [users] = await db.execute( | ||||||
|  |       'SELECT payment_status FROM users WHERE id = ?', | ||||||
|  |       [userId] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     if (users.length === 0) { | ||||||
|  |       return res.status(404).json({ | ||||||
|  |         success: false, | ||||||
|  |         message: '用户不存在' | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const paymentStatus = users[0].payment_status; | ||||||
|  |  | ||||||
|  |     // 查询最近的支付订单 | ||||||
|  |     const [recentOrders] = await db.execute( | ||||||
|  |       `SELECT out_trade_no, trade_type, status, total_fee, paid_at  | ||||||
|  |        FROM payment_orders  | ||||||
|  |        WHERE user_id = ?  | ||||||
|  |        ORDER BY created_at DESC  | ||||||
|  |        LIMIT 1`, | ||||||
|  |       [userId] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     res.json({ | ||||||
|  |       success: true, | ||||||
|  |       data: { | ||||||
|  |         paymentStatus, | ||||||
|  |         isPaid: paymentStatus === 'paid', | ||||||
|  |         recentOrder: recentOrders.length > 0 ? { | ||||||
|  |           ...recentOrders[0], | ||||||
|  |           total_fee: recentOrders[0].total_fee / 100, | ||||||
|  |           payment_method_name: recentOrders[0].trade_type.startsWith('ALIPAY') ? '支付宝支付' : '微信支付' | ||||||
|  |         } : null | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('检查用户支付状态失败:', error); | ||||||
|  |     res.status(500).json({ | ||||||
|  |       success: false, | ||||||
|  |       message: '检查支付状态失败' | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 支付宝支付回调通知 | ||||||
|  |  * POST /api/payment/alipay/notify | ||||||
|  |  */ | ||||||
|  | router.post('/alipay/notify', async (req, res) => { | ||||||
|  |   try { | ||||||
|  |     console.log('收到支付宝支付回调:', req.body); | ||||||
|  |      | ||||||
|  |     // 验证签名 | ||||||
|  |     const isValid = alipayService.verifyNotifySign(req.body); | ||||||
|  |     if (!isValid) { | ||||||
|  |       console.error('支付宝回调签名验证失败'); | ||||||
|  |       return res.status(400).send('FAIL'); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     const { | ||||||
|  |       out_trade_no: outTradeNo, | ||||||
|  |       trade_no: transactionId, | ||||||
|  |       trade_status: tradeStatus, | ||||||
|  |       total_amount: totalAmount | ||||||
|  |     } = req.body; | ||||||
|  |      | ||||||
|  |     // 只处理支付成功的回调 | ||||||
|  |     if (tradeStatus === 'TRADE_SUCCESS') { | ||||||
|  |       const db = getDB(); | ||||||
|  |        | ||||||
|  |       // 检查订单是否存在 | ||||||
|  |       const [orders] = await db.execute( | ||||||
|  |         'SELECT id, user_id, status FROM payment_orders WHERE out_trade_no = ?', | ||||||
|  |         [outTradeNo] | ||||||
|  |       ); | ||||||
|  |        | ||||||
|  |       if (orders.length === 0) { | ||||||
|  |         console.error('支付宝回调:订单不存在', outTradeNo); | ||||||
|  |         return res.status(400).send('FAIL'); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       const order = orders[0]; | ||||||
|  |        | ||||||
|  |       // 如果订单已经处理过,直接返回成功 | ||||||
|  |       if (order.status === 'paid') { | ||||||
|  |         console.log('支付宝回调:订单已处理', outTradeNo); | ||||||
|  |         return res.send('SUCCESS'); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // 更新订单状态 | ||||||
|  |       await alipayService.updatePaymentStatus(outTradeNo, { | ||||||
|  |         status: 'paid', | ||||||
|  |         transactionId, | ||||||
|  |         paidAt: new Date() | ||||||
|  |       }); | ||||||
|  |        | ||||||
|  |       console.log('支付宝支付成功处理完成:', { | ||||||
|  |         outTradeNo, | ||||||
|  |         transactionId, | ||||||
|  |         userId: order.user_id | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     res.send('SUCCESS'); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('处理支付宝支付回调失败:', error); | ||||||
|  |     res.status(500).send('FAIL'); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 支付宝支付返回页面处理 | ||||||
|  |  * GET /api/payment/alipay/return | ||||||
|  |  */ | ||||||
|  | router.get('/alipay/return', async (req, res) => { | ||||||
|  |   try { | ||||||
|  |     console.log('支付宝支付返回:', req.query); | ||||||
|  |      | ||||||
|  |     // 验证签名 | ||||||
|  |     const isValid = alipayService.verifyNotifySign(req.query); | ||||||
|  |     if (!isValid) { | ||||||
|  |       console.error('支付宝返回签名验证失败'); | ||||||
|  |       return res.redirect('/payment/failed'); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     const { out_trade_no: outTradeNo } = req.query; | ||||||
|  |      | ||||||
|  |     // 重定向到支付成功页面 | ||||||
|  |     res.redirect(`/payment/success?outTradeNo=${outTradeNo}`); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('处理支付宝支付返回失败:', error); | ||||||
|  |     res.redirect('/payment/failed'); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | module.exports = router; | ||||||
| @@ -120,40 +120,50 @@ router.get('/zhejiang', async (req, res) => { | |||||||
|  */ |  */ | ||||||
| router.get('/provinces', async (req, res) => { | router.get('/provinces', async (req, res) => { | ||||||
|   try { |   try { | ||||||
|     // 递归获取子区域的函数 |     // 一次性获取所有区域数据(省、市、区县) | ||||||
|     async function getChildrenRecursively(parentCode, level) { |     const [allRegions] = await getDB().execute( | ||||||
|       const [children] = await getDB().execute( |       `SELECT code, name as label, level, parent_code FROM china_regions  | ||||||
|         `SELECT code, name as label, level FROM china_regions  |        WHERE level <= 3 | ||||||
|          WHERE parent_code = ? AND level = ? |        ORDER BY level, code` | ||||||
|          ORDER BY code`, |  | ||||||
|         [parentCode, level] |  | ||||||
|     ); |     ); | ||||||
|      |      | ||||||
|       // 为每个子区域递归获取其子区域 |     // 按level分组数据 | ||||||
|       for (let child of children) { |     const regionsByLevel = { | ||||||
|         if (level < 3) { // 最多到区县级别(level 3) |       1: [], // 省份 | ||||||
|           child.children = await getChildrenRecursively(child.code, level + 1); |       2: [], // 城市 | ||||||
|         } |       3: []  // 区县 | ||||||
|       } |     }; | ||||||
|      |      | ||||||
|       return children; |     // 创建code到region的映射,便于快速查找 | ||||||
|  |     const regionMap = {}; | ||||||
|  |      | ||||||
|  |     // 分组并建立映射 | ||||||
|  |     allRegions.forEach(region => { | ||||||
|  |       region.children = []; // 初始化children数组 | ||||||
|  |       regionsByLevel[region.level].push(region); | ||||||
|  |       regionMap[region.code] = region; | ||||||
|  |     }); | ||||||
|  |      | ||||||
|  |     // 构建层级关系:先处理区县到城市的关系 | ||||||
|  |     regionsByLevel[3].forEach(district => { | ||||||
|  |       const parentCity = regionMap[district.parent_code]; | ||||||
|  |       if (parentCity) { | ||||||
|  |         parentCity.children.push(district); | ||||||
|       } |       } | ||||||
|  |     }); | ||||||
|      |      | ||||||
|     // 获取所有省份 |     // 再处理城市到省份的关系 | ||||||
|     const [provinces] = await getDB().execute( |     regionsByLevel[2].forEach(city => { | ||||||
|       `SELECT code, name as label, level FROM china_regions  |       const parentProvince = regionMap[city.parent_code]; | ||||||
|        WHERE level = 1 |       if (parentProvince) { | ||||||
|        ORDER BY code` |         parentProvince.children.push(city); | ||||||
|     ); |  | ||||||
|      |  | ||||||
|     // 为每个省份递归获取城市和区县 |  | ||||||
|     for (let province of provinces) { |  | ||||||
|       province.children = await getChildrenRecursively(province.code, 2); |  | ||||||
|       } |       } | ||||||
|  |     }); | ||||||
|      |      | ||||||
|  |     // 返回省份数据(已包含完整的层级结构) | ||||||
|     res.json({ |     res.json({ | ||||||
|       success: true, |       success: true, | ||||||
|       data: provinces |       data: regionsByLevel[1] | ||||||
|     }); |     }); | ||||||
|   } catch (error) { |   } catch (error) { | ||||||
|     console.error('获取省份列表错误:', error); |     console.error('获取省份列表错误:', error); | ||||||
|   | |||||||
| @@ -138,18 +138,6 @@ router.post('/send', async (req, res) => { | |||||||
|      |      | ||||||
|     // 记录发送时间 |     // 记录发送时间 | ||||||
|     smsCodeStore.set(`last_send_${phone}`, Date.now()) |     smsCodeStore.set(`last_send_${phone}`, Date.now()) | ||||||
|      |  | ||||||
|     // 开发环境下模拟发送成功 |  | ||||||
|     // if (SMS_CONFIG.isDevelopment) { |  | ||||||
|     //   console.log(`[开发环境] 短信验证码: ${code} 发送到 ${phone}`) |  | ||||||
|     //   return res.json({ |  | ||||||
|     //     success: true, |  | ||||||
|     //     message: '验证码发送成功', |  | ||||||
|     //     // 开发环境下返回验证码便于测试 |  | ||||||
|     //     code: process.env.NODE_ENV === 'development' ? code : undefined |  | ||||||
|     //   }) |  | ||||||
|     // } |  | ||||||
|      |  | ||||||
|     // 生产环境发送真实短信 |     // 生产环境发送真实短信 | ||||||
|     try { |     try { | ||||||
|       const sendSmsRequest = new Dysmsapi20170525.SendSmsRequest({ |       const sendSmsRequest = new Dysmsapi20170525.SendSmsRequest({ | ||||||
|   | |||||||
| @@ -1234,6 +1234,11 @@ router.post('/force-change-status/:transferId', authenticateToken, async (req, r | |||||||
|      |      | ||||||
|     const { transferId } = req.params; |     const { transferId } = req.params; | ||||||
|     const { newStatus, status, reason, adjust_balance = false } = req.body; |     const { newStatus, status, reason, adjust_balance = false } = req.body; | ||||||
|  |     console.log('newStatus:', newStatus); | ||||||
|  |     console.log('status:', status); | ||||||
|  |     console.log('reason:', reason); | ||||||
|  |     console.log('adjust_balance:', adjust_balance); | ||||||
|  |      | ||||||
|     // 兼容两种参数名:newStatus 和 status |     // 兼容两种参数名:newStatus 和 status | ||||||
|     const actualNewStatus = newStatus || status; |     const actualNewStatus = newStatus || status; | ||||||
|     const adminId = req.user.id; |     const adminId = req.user.id; | ||||||
| @@ -1248,9 +1253,9 @@ router.post('/force-change-status/:transferId', authenticateToken, async (req, r | |||||||
|       return res.status(400).json({ success: false, message: '新状态不能为空' }); |       return res.status(400).json({ success: false, message: '新状态不能为空' }); | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     if (!reason) { |     // if (!reason) { | ||||||
|       return res.status(400).json({ success: false, message: '变更原因不能为空' }); |     //   return res.status(400).json({ success: false, message: '变更原因不能为空' }); | ||||||
|     } |     // } | ||||||
|      |      | ||||||
|     const result = await transferService.forceChangeTransferStatus( |     const result = await transferService.forceChangeTransferStatus( | ||||||
|       transferId,  |       transferId,  | ||||||
|   | |||||||
							
								
								
									
										202
									
								
								routes/wechatPay.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								routes/wechatPay.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,202 @@ | |||||||
|  | const express = require('express'); | ||||||
|  | const router = express.Router(); | ||||||
|  | const WechatPayService = require('../services/wechatPayService'); | ||||||
|  | const { getDB } = require('../database'); | ||||||
|  | const { auth, paymentAuth } = require('../middleware/auth'); | ||||||
|  |  | ||||||
|  | // 创建微信支付服务实例 | ||||||
|  | const wechatPayService = new WechatPayService(); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 创建注册支付订单 (API v3 - H5支付) | ||||||
|  |  * POST /api/wechat-pay/create-registration-order | ||||||
|  |  */ | ||||||
|  | router.post('/create-registration-order', paymentAuth, async (req, res) => { | ||||||
|  |   try { | ||||||
|  |     const userId = req.user.id; | ||||||
|  |     const username = req.user.username; | ||||||
|  |     const phone = req.user.phone; | ||||||
|  |      | ||||||
|  |     // 获取客户端IP | ||||||
|  |     const clientIp = req.headers['x-forwarded-for'] ||  | ||||||
|  |                     req.headers['x-real-ip'] ||  | ||||||
|  |                     req.connection.remoteAddress ||  | ||||||
|  |                     req.socket.remoteAddress || | ||||||
|  |                     (req.connection.socket ? req.connection.socket.remoteAddress : null) || | ||||||
|  |                     '127.0.0.1'; | ||||||
|  |  | ||||||
|  |     // 检查用户是否已经支付过 | ||||||
|  |     const db = getDB(); | ||||||
|  |     const [existingOrders] = await db.execute( | ||||||
|  |       'SELECT id FROM payment_orders WHERE user_id = ? AND status = "paid"', | ||||||
|  |       [userId] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     if (existingOrders.length > 0) { | ||||||
|  |       return res.status(400).json({ | ||||||
|  |         success: false, | ||||||
|  |         message: '用户已完成支付,无需重复支付' | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     console.log('创建H5支付订单:', { | ||||||
|  |       userId, | ||||||
|  |       username, | ||||||
|  |       phone, | ||||||
|  |       clientIp | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // 创建H5支付订单 | ||||||
|  |     const result = await wechatPayService.createRegistrationPayOrder({ | ||||||
|  |       userId, | ||||||
|  |       username, | ||||||
|  |       phone, | ||||||
|  |       clientIp | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     if (result.success) { | ||||||
|  |       res.json({ | ||||||
|  |         success: true, | ||||||
|  |         data: { | ||||||
|  |           outTradeNo: result.data.outTradeNo, | ||||||
|  |           h5Url: result.data.h5Url, | ||||||
|  |           paymentType: result.data.paymentType | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     } else { | ||||||
|  |       res.status(500).json({ | ||||||
|  |         success: false, | ||||||
|  |         message: '创建支付订单失败' | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('创建H5支付订单异常:', error); | ||||||
|  |     res.status(500).json({ | ||||||
|  |       success: false, | ||||||
|  |       message: error.message || '服务器内部错误' | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | // H5支付不需要获取openid,移除相关接口 | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 微信支付回调接口 (API v3) | ||||||
|  |  * POST /api/wechat-pay/notify | ||||||
|  |  */ | ||||||
|  | router.post('/notify', async (req, res) => { | ||||||
|  |   try { | ||||||
|  |     // API v3 回调是JSON格式 | ||||||
|  |     const notifyData = req.body; | ||||||
|  |      | ||||||
|  |     // 获取请求头中的签名信息 | ||||||
|  |     const signature = req.headers['wechatpay-signature']; | ||||||
|  |     const timestamp = req.headers['wechatpay-timestamp']; | ||||||
|  |     const nonce = req.headers['wechatpay-nonce']; | ||||||
|  |     const serial = req.headers['wechatpay-serial']; | ||||||
|  |      | ||||||
|  |     console.log('收到API v3支付回调:', { | ||||||
|  |       signature, | ||||||
|  |       timestamp, | ||||||
|  |       nonce, | ||||||
|  |       serial, | ||||||
|  |       body: notifyData | ||||||
|  |     }); | ||||||
|  |      | ||||||
|  |     // 验证签名和处理回调 | ||||||
|  |     const result = await wechatPayService.handleV3PaymentNotify({ | ||||||
|  |       signature, | ||||||
|  |       timestamp, | ||||||
|  |       nonce, | ||||||
|  |       serial, | ||||||
|  |       body: JSON.stringify(notifyData) | ||||||
|  |     }); | ||||||
|  |      | ||||||
|  |     if (result.success) { | ||||||
|  |       // API v3 成功响应 | ||||||
|  |       res.status(200).json({ code: 'SUCCESS', message: '成功' }); | ||||||
|  |     } else { | ||||||
|  |       // API v3 失败响应 | ||||||
|  |       res.status(400).json({ code: 'FAIL', message: result.message || '处理失败' }); | ||||||
|  |     } | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('支付回调处理异常:', error); | ||||||
|  |     res.status(500).json({ code: 'ERROR', message: '服务器内部错误' }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 查询支付状态 | ||||||
|  |  * GET /api/wechat-pay/query-status/:outTradeNo | ||||||
|  |  */ | ||||||
|  | router.get('/query-status/:outTradeNo', paymentAuth, async (req, res) => { | ||||||
|  |   try { | ||||||
|  |     const { outTradeNo } = req.params; | ||||||
|  |     const userId = req.user.id; | ||||||
|  |  | ||||||
|  |     // 验证订单是否属于当前用户 | ||||||
|  |     const db = getDB(); | ||||||
|  |     const [orders] = await db.execute( | ||||||
|  |       'SELECT id FROM payment_orders WHERE out_trade_no = ? AND user_id = ?', | ||||||
|  |       [outTradeNo, userId] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     if (orders.length === 0) { | ||||||
|  |       return res.status(404).json({ | ||||||
|  |         success: false, | ||||||
|  |         message: '订单不存在或无权限访问' | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const result = await wechatPayService.queryPaymentStatus(outTradeNo); | ||||||
|  |     res.json(result); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('查询支付状态失败:', error); | ||||||
|  |     res.status(500).json({ | ||||||
|  |       success: false, | ||||||
|  |       message: error.message || '查询支付状态失败' | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 检查用户支付状态 | ||||||
|  |  * GET /api/wechat-pay/check-user-payment | ||||||
|  |  */ | ||||||
|  | router.get('/check-user-payment', auth, async (req, res) => { | ||||||
|  |   try { | ||||||
|  |     const userId = req.user.id; | ||||||
|  |     const db = getDB(); | ||||||
|  |  | ||||||
|  |     // 查询用户支付状态 | ||||||
|  |     const [users] = await db.execute( | ||||||
|  |       'SELECT payment_status FROM users WHERE id = ?', | ||||||
|  |       [userId] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     if (users.length === 0) { | ||||||
|  |       return res.status(404).json({ | ||||||
|  |         success: false, | ||||||
|  |         message: '用户不存在' | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const paymentStatus = users[0].payment_status; | ||||||
|  |  | ||||||
|  |     res.json({ | ||||||
|  |       success: true, | ||||||
|  |       data: { | ||||||
|  |         paymentStatus, | ||||||
|  |         isPaid: paymentStatus === 'paid' | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('检查用户支付状态失败:', error); | ||||||
|  |     res.status(500).json({ | ||||||
|  |       success: false, | ||||||
|  |       message: '检查支付状态失败' | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | module.exports = router; | ||||||
							
								
								
									
										87
									
								
								run_migration.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								run_migration.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | |||||||
|  | const mysql = require('mysql2/promise'); | ||||||
|  | const fs = require('fs'); | ||||||
|  | const path = require('path'); | ||||||
|  | require('dotenv').config(); | ||||||
|  |  | ||||||
|  | async function runMigration() { | ||||||
|  |   let connection; | ||||||
|  |    | ||||||
|  |   try { | ||||||
|  |     // 创建数据库连接 | ||||||
|  |     connection = await mysql.createConnection({ | ||||||
|  |       host: process.env.DB_HOST, | ||||||
|  |       user: process.env.DB_USER, | ||||||
|  |       password: process.env.DB_PASSWORD, | ||||||
|  |       database: process.env.DB_NAME, | ||||||
|  |       charset: 'utf8mb4' | ||||||
|  |     }); | ||||||
|  |      | ||||||
|  |     console.log('数据库连接成功'); | ||||||
|  |      | ||||||
|  |     // 直接定义SQL语句 | ||||||
|  |     const sqlStatements = [ | ||||||
|  |       `CREATE TABLE IF NOT EXISTS \`payment_orders\` ( | ||||||
|  |         \`id\` int(11) NOT NULL AUTO_INCREMENT COMMENT '订单ID', | ||||||
|  |         \`user_id\` int(11) NOT NULL COMMENT '用户ID', | ||||||
|  |         \`out_trade_no\` varchar(64) NOT NULL COMMENT '商户订单号', | ||||||
|  |         \`transaction_id\` varchar(64) DEFAULT NULL COMMENT '微信支付订单号', | ||||||
|  |         \`total_fee\` int(11) NOT NULL COMMENT '订单金额(分)', | ||||||
|  |         \`body\` varchar(128) NOT NULL COMMENT '商品描述', | ||||||
|  |         \`trade_type\` varchar(16) NOT NULL COMMENT '交易类型', | ||||||
|  |         \`prepay_id\` varchar(64) DEFAULT NULL COMMENT '预支付交易会话标识', | ||||||
|  |         \`mweb_url\` text DEFAULT NULL COMMENT 'H5支付跳转链接', | ||||||
|  |         \`status\` enum('pending','paid','failed','cancelled') NOT NULL DEFAULT 'pending' COMMENT '支付状态', | ||||||
|  |         \`paid_at\` datetime DEFAULT NULL COMMENT '支付完成时间', | ||||||
|  |         \`created_at\` datetime NOT NULL COMMENT '创建时间', | ||||||
|  |         \`updated_at\` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', | ||||||
|  |         PRIMARY KEY (\`id\`), | ||||||
|  |         UNIQUE KEY \`uk_out_trade_no\` (\`out_trade_no\`), | ||||||
|  |         KEY \`idx_user_id\` (\`user_id\`), | ||||||
|  |         KEY \`idx_status\` (\`status\`), | ||||||
|  |         KEY \`idx_created_at\` (\`created_at\`) | ||||||
|  |       ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付订单表'`, | ||||||
|  |        | ||||||
|  |       `ALTER TABLE \`users\` ADD COLUMN \`payment_status\` enum('unpaid','paid') NOT NULL DEFAULT 'unpaid' COMMENT '支付状态'`, | ||||||
|  |        | ||||||
|  |       `ALTER TABLE \`users\` ADD KEY \`idx_payment_status\` (\`payment_status\`)` | ||||||
|  |     ]; | ||||||
|  |      | ||||||
|  |     console.log(`准备执行 ${sqlStatements.length} 条SQL语句`); | ||||||
|  |      | ||||||
|  |     // 执行每条SQL语句 | ||||||
|  |     for (let i = 0; i < sqlStatements.length; i++) { | ||||||
|  |       const statement = sqlStatements[i]; | ||||||
|  |       console.log(`执行第 ${i + 1} 条语句...`); | ||||||
|  |        | ||||||
|  |       try { | ||||||
|  |         await connection.execute(statement); | ||||||
|  |         console.log(`✓ 第 ${i + 1} 条语句执行成功`); | ||||||
|  |       } catch (error) { | ||||||
|  |         if (error.code === 'ER_DUP_FIELDNAME') { | ||||||
|  |           console.log(`⚠ 第 ${i + 1} 条语句跳过(字段已存在): ${error.message}`); | ||||||
|  |         } else if (error.code === 'ER_TABLE_EXISTS_ERROR') { | ||||||
|  |           console.log(`⚠ 第 ${i + 1} 条语句跳过(表已存在): ${error.message}`); | ||||||
|  |         } else if (error.code === 'ER_DUP_KEYNAME') { | ||||||
|  |           console.log(`⚠ 第 ${i + 1} 条语句跳过(索引已存在): ${error.message}`); | ||||||
|  |         } else { | ||||||
|  |           console.error(`✗ 第 ${i + 1} 条语句执行失败:`, error.message); | ||||||
|  |           throw error; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     console.log('\n✅ 数据库迁移完成!'); | ||||||
|  |      | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('❌ 数据库迁移失败:', error.message); | ||||||
|  |     process.exit(1); | ||||||
|  |   } finally { | ||||||
|  |     if (connection) { | ||||||
|  |       await connection.end(); | ||||||
|  |       console.log('数据库连接已关闭'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 运行迁移 | ||||||
|  | runMigration(); | ||||||
| @@ -40,6 +40,7 @@ app.use(cors({ | |||||||
|   origin: [ |   origin: [ | ||||||
|     'http://localhost:5173',  |     'http://localhost:5173',  | ||||||
|     'http://localhost:5176',  |     'http://localhost:5176',  | ||||||
|  |     'http://localhost:5175',  | ||||||
|     'http://localhost:5174',  |     'http://localhost:5174',  | ||||||
|     'http://localhost:3001', |     'http://localhost:3001', | ||||||
|     'https://www.zrbjr.com', |     'https://www.zrbjr.com', | ||||||
| @@ -250,6 +251,9 @@ app.use('/api/regions', require('./routes/regions')); | |||||||
| app.use('/api/addresses', require('./routes/addresses')); | app.use('/api/addresses', require('./routes/addresses')); | ||||||
| app.use('/api/address-labels', require('./routes/address-labels')); | app.use('/api/address-labels', require('./routes/address-labels')); | ||||||
| app.use('/api/cart', require('./routes/cart')); | app.use('/api/cart', require('./routes/cart')); | ||||||
|  | app.use('/api/announcements', require('./routes/announcements')); // 通知公告路由 | ||||||
|  | app.use('/api/wechat-pay', require('./routes/wechatPay')); // 只保留微信支付 | ||||||
|  | app.use('/api/payment', require('./routes/payment')); | ||||||
|  |  | ||||||
| // 前端路由 - 必须在最后,作为fallback | // 前端路由 - 必须在最后,作为fallback | ||||||
| app.get('/', (req, res) => { | app.get('/', (req, res) => { | ||||||
|   | |||||||
							
								
								
									
										224
									
								
								services/alipayservice.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										224
									
								
								services/alipayservice.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,224 @@ | |||||||
|  | const { AlipaySdk } = require('alipay-sdk'); | ||||||
|  | const { getDB } = require('../database'); | ||||||
|  | const crypto = require('crypto'); | ||||||
|  | const path = require('path'); | ||||||
|  | const fs = require('fs'); | ||||||
|  |  | ||||||
|  | class AlipayService { | ||||||
|  |   constructor() { | ||||||
|  |     // 读取密钥文件 | ||||||
|  |     const privateKeyPath = path.join(__dirname, '../certs/alipay-private-key.pem'); | ||||||
|  |     const publicKeyPath = path.join(__dirname, '../certs/alipay-public-key.pem'); | ||||||
|  |      | ||||||
|  |     const privateKey = fs.readFileSync(privateKeyPath, 'utf8'); | ||||||
|  |     const alipayPublicKey = fs.readFileSync(publicKeyPath, 'utf8'); | ||||||
|  |      | ||||||
|  |     // 支付宝配置 | ||||||
|  |     this.config = { | ||||||
|  |       appId: process.env.ALIPAY_APP_ID || '2021001161683774', // 替换为实际的应用ID | ||||||
|  |       privateKey: privateKey, // 从文件读取的应用私钥 | ||||||
|  |       alipayPublicKey: alipayPublicKey, // 从文件读取的支付宝公钥 | ||||||
|  |       gateway: 'https://openapi.alipay.com/gateway.do', // 支付宝网关地址 | ||||||
|  |       signType: 'RSA2', | ||||||
|  |       charset: 'utf-8', | ||||||
|  |       version: '1.0', | ||||||
|  |       timeout: 5000 | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // 初始化支付宝SDK | ||||||
|  |     this.alipaySdk = new AlipaySdk({ | ||||||
|  |       appId: this.config.appId, | ||||||
|  |       privateKey: this.config.privateKey, | ||||||
|  |       alipayPublicKey: this.config.alipayPublicKey, | ||||||
|  |       gateway: this.config.gateway, | ||||||
|  |       signType: this.config.signType, | ||||||
|  |       timeout: this.config.timeout | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 创建注册支付订单 | ||||||
|  |    * @param {Object} params - 支付参数 | ||||||
|  |    * @param {string} params.userId - 用户ID | ||||||
|  |    * @param {string} params.username - 用户名 | ||||||
|  |    * @param {string} params.phone - 手机号 | ||||||
|  |    * @param {string} params.clientIp - 客户端IP | ||||||
|  |    * @returns {Promise<Object>} 支付结果 | ||||||
|  |    */ | ||||||
|  |   async createRegistrationPayOrder({ userId, username, phone, clientIp }) { | ||||||
|  |     try { | ||||||
|  |       const db = getDB(); | ||||||
|  |        | ||||||
|  |       // 生成订单号 | ||||||
|  |       const outTradeNo = this.generateOrderNo(); | ||||||
|  |       const totalFee = 39900; // 399元,单位:分 | ||||||
|  |       const subject = '用户注册激活费用'; | ||||||
|  |       const body = `用户${username}(${phone})注册激活费用`; | ||||||
|  |        | ||||||
|  |       // 业务参数 | ||||||
|  |       const bizContent = { | ||||||
|  |         out_trade_no: outTradeNo, | ||||||
|  |         total_amount: (totalFee / 100).toFixed(2), // 转换为元 | ||||||
|  |         subject: subject, | ||||||
|  |         body: body, | ||||||
|  |         product_code: 'QUICK_WAP_WAY', | ||||||
|  |         quit_url: process.env.ALIPAY_QUIT_URL || 'https://your-domain.com/payment/cancel' | ||||||
|  |       }; | ||||||
|  |        | ||||||
|  |       // 使用新版SDK的pageExecute方法生成支付URL | ||||||
|  |       const payUrl = this.alipaySdk.pageExecute('alipay.trade.wap.pay', 'GET', { | ||||||
|  |         bizContent: bizContent, | ||||||
|  |         notifyUrl: process.env.ALIPAY_NOTIFY_URL || 'https://your-domain.com/api/payment/alipay/notify', | ||||||
|  |         returnUrl: process.env.ALIPAY_RETURN_URL || 'https://your-domain.com/payment/success' | ||||||
|  |       }); | ||||||
|  |        | ||||||
|  |       // 保存订单到数据库 | ||||||
|  |       await db.execute( | ||||||
|  |         `INSERT INTO payment_orders  | ||||||
|  |          (user_id, out_trade_no, total_fee, body, trade_type, status, created_at)  | ||||||
|  |          VALUES (?, ?, ?, ?, ?, ?, NOW())`, | ||||||
|  |         [userId, outTradeNo, totalFee, body, 'ALIPAY_WAP', 'pending'] | ||||||
|  |       ); | ||||||
|  |        | ||||||
|  |       console.log('支付宝支付订单创建成功:', { | ||||||
|  |         userId, | ||||||
|  |         outTradeNo, | ||||||
|  |         totalFee, | ||||||
|  |         payUrl | ||||||
|  |       }); | ||||||
|  |        | ||||||
|  |       return { | ||||||
|  |         success: true, | ||||||
|  |         data: { | ||||||
|  |           outTradeNo, | ||||||
|  |           payUrl, | ||||||
|  |           paymentType: 'alipay_wap', | ||||||
|  |           totalFee | ||||||
|  |         } | ||||||
|  |       }; | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('创建支付宝支付订单失败:', error); | ||||||
|  |       return { | ||||||
|  |         success: false, | ||||||
|  |         message: error.message || '创建支付订单失败' | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 查询支付状态 | ||||||
|  |    * @param {string} outTradeNo - 商户订单号 | ||||||
|  |    * @returns {Promise<Object>} 查询结果 | ||||||
|  |    */ | ||||||
|  |   async queryPaymentStatus(outTradeNo) { | ||||||
|  |     try { | ||||||
|  |       const result = await this.alipaySdk.exec('alipay.trade.query', { | ||||||
|  |         bizContent: { | ||||||
|  |           out_trade_no: outTradeNo | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |        | ||||||
|  |       if (result.code === '10000') { | ||||||
|  |         // 查询成功 | ||||||
|  |         const tradeStatus = result.tradeStatus; | ||||||
|  |          | ||||||
|  |         // 如果支付成功,更新数据库 | ||||||
|  |         if (tradeStatus === 'TRADE_SUCCESS') { | ||||||
|  |           await this.updatePaymentStatus(outTradeNo, { | ||||||
|  |             status: 'paid', | ||||||
|  |             transactionId: result.tradeNo, | ||||||
|  |             paidAt: new Date() | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         return { | ||||||
|  |           success: true, | ||||||
|  |           data: { | ||||||
|  |             trade_status: tradeStatus, | ||||||
|  |             trade_no: result.tradeNo, | ||||||
|  |             total_amount: result.totalAmount, | ||||||
|  |             buyer_pay_amount: result.buyerPayAmount, | ||||||
|  |             gmt_payment: result.gmtPayment | ||||||
|  |           } | ||||||
|  |         }; | ||||||
|  |       } else { | ||||||
|  |         return { | ||||||
|  |           success: false, | ||||||
|  |           message: result.msg || '查询支付状态失败' | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('查询支付宝支付状态失败:', error); | ||||||
|  |       return { | ||||||
|  |         success: false, | ||||||
|  |         message: error.message || '查询支付状态失败' | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 更新支付状态 | ||||||
|  |    * @param {string} outTradeNo - 商户订单号 | ||||||
|  |    * @param {Object} updateData - 更新数据 | ||||||
|  |    */ | ||||||
|  |   async updatePaymentStatus(outTradeNo, updateData) { | ||||||
|  |     try { | ||||||
|  |       const db = getDB(); | ||||||
|  |        | ||||||
|  |       // 更新订单状态 | ||||||
|  |       await db.execute( | ||||||
|  |         `UPDATE payment_orders  | ||||||
|  |          SET status = ?, transaction_id = ?, paid_at = ?  | ||||||
|  |          WHERE out_trade_no = ?`, | ||||||
|  |         [updateData.status, updateData.transactionId, updateData.paidAt, outTradeNo] | ||||||
|  |       ); | ||||||
|  |        | ||||||
|  |       // 如果支付成功,更新用户支付状态 | ||||||
|  |       if (updateData.status === 'paid') { | ||||||
|  |         const [orders] = await db.execute( | ||||||
|  |           'SELECT user_id FROM payment_orders WHERE out_trade_no = ?', | ||||||
|  |           [outTradeNo] | ||||||
|  |         ); | ||||||
|  |          | ||||||
|  |         if (orders.length > 0) { | ||||||
|  |           const userId = orders[0].user_id; | ||||||
|  |           await db.execute( | ||||||
|  |             'UPDATE users SET payment_status = ? WHERE id = ?', | ||||||
|  |             ['paid', userId] | ||||||
|  |           ); | ||||||
|  |            | ||||||
|  |           console.log('用户支付状态更新成功:', { userId, outTradeNo }); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('更新支付状态失败:', error); | ||||||
|  |       throw error; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 验证支付宝回调签名 | ||||||
|  |    * @param {Object} params - 回调参数 | ||||||
|  |    * @returns {boolean} 验证结果 | ||||||
|  |    */ | ||||||
|  |   verifyNotifySign(params) { | ||||||
|  |     try { | ||||||
|  |       return this.alipaySdk.checkNotifySign(params); | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('验证支付宝回调签名失败:', error); | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 生成订单号 | ||||||
|  |    * @returns {string} 订单号 | ||||||
|  |    */ | ||||||
|  |   generateOrderNo() { | ||||||
|  |     const timestamp = Date.now(); | ||||||
|  |     const random = Math.floor(Math.random() * 1000).toString().padStart(3, '0'); | ||||||
|  |     return `ALI${timestamp}${random}`; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = AlipayService; | ||||||
| @@ -851,7 +851,8 @@ class MatchingService { | |||||||
|             historicalNetBalance: user.historical_net_balance, |             historicalNetBalance: user.historical_net_balance, | ||||||
|             totalPendingInflow: user.total_pending_inflow, |             totalPendingInflow: user.total_pending_inflow, | ||||||
|             availableForAllocation: user.available_for_allocation, |             availableForAllocation: user.available_for_allocation, | ||||||
|             todayOutflow: user.today_outflow |             todayOutflow: user.today_outflow, | ||||||
|  |             has_active_allocations:user.has_active_allocations | ||||||
|           }); |           }); | ||||||
|           remainingAmount -= maxUserAllocation; |           remainingAmount -= maxUserAllocation; | ||||||
|         } |         } | ||||||
| @@ -871,7 +872,7 @@ class MatchingService { | |||||||
|           // 计算每个用户的剩余可分配容量 |           // 计算每个用户的剩余可分配容量 | ||||||
|           for (const allocation of allocations) { |           for (const allocation of allocations) { | ||||||
|             // 获取用户当前的实际余额状态(使用has_active_allocations作为实际可分配余额) |             // 获取用户当前的实际余额状态(使用has_active_allocations作为实际可分配余额) | ||||||
|             const maxSafeAmount = Math.abs(allocation.availableForAllocation); |             const maxSafeAmount = Math.abs(allocation.has_active_allocations); | ||||||
|             const remainingCapacity = maxSafeAmount - allocation.amount; |             const remainingCapacity = maxSafeAmount - allocation.amount; | ||||||
|              |              | ||||||
|             if (remainingCapacity > 0) { |             if (remainingCapacity > 0) { | ||||||
|   | |||||||
| @@ -862,14 +862,15 @@ class TransferService { | |||||||
|                          |                          | ||||||
|                         // 根据所有相关transfers的状态来决定matching_order的状态 |                         // 根据所有相关transfers的状态来决定matching_order的状态 | ||||||
|                         const transferStatuses = allTransfers.map(t => t.status); |                         const transferStatuses = allTransfers.map(t => t.status); | ||||||
|  |                         console.log(transferStatuses,'transferStatuses'); | ||||||
|                          |                          | ||||||
|                         if (transferStatuses.every(status => status === 'cancelled' || status === 'rejected' || status === 'not_received')) { |                         if (transferStatuses.every(status => status === 'cancelled' || status === 'rejected' || status === 'not_received' || status === 'confirmed' || status === 'received')) { | ||||||
|                              // 如果所有transfers都被取消/拒绝/未收到,匹配订单标记为已完成 |                              // 如果所有transfers都被取消/拒绝/未收到,匹配订单标记为已完成 | ||||||
|                              matchingOrderStatus = 'completed'; |                              matchingOrderStatus = 'completed'; | ||||||
|                          } else if (transferStatuses.every(status => status === 'received')) { |                          } else if (transferStatuses.every(status => status === 'received')) { | ||||||
|                              // 如果所有transfers都已收到,匹配订单完成 |                              // 如果所有transfers都已收到,匹配订单完成 | ||||||
|                              matchingOrderStatus = 'completed'; |                              matchingOrderStatus = 'completed'; | ||||||
|                          } else if (transferStatuses.includes('cancelled') || transferStatuses.includes('rejected') || transferStatuses.includes('not_received') || transferStatuses.some(status => status === 'confirmed' || status === 'received')) { |                          } else if (transferStatuses.includes('cancelled') || transferStatuses.includes('rejected') || transferStatuses.includes('not_received') ) { | ||||||
|                              // 如果有任何一个transfer被取消/拒绝/未收到,或者有transfers已确认或已收到,匹配订单为进行中状态 |                              // 如果有任何一个transfer被取消/拒绝/未收到,或者有transfers已确认或已收到,匹配订单为进行中状态 | ||||||
|                              matchingOrderStatus = 'matching'; |                              matchingOrderStatus = 'matching'; | ||||||
|                          } else { |                          } else { | ||||||
|   | |||||||
							
								
								
									
										566
									
								
								services/wechatPayService.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										566
									
								
								services/wechatPayService.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,566 @@ | |||||||
|  | const crypto = require('crypto'); | ||||||
|  | const axios = require('axios'); | ||||||
|  | const fs = require('fs'); | ||||||
|  | const path = require('path'); | ||||||
|  | const { wechatPay } = require('../config/wechatPay'); | ||||||
|  | const { getDB } = require('../database'); | ||||||
|  |  | ||||||
|  | class WechatPayService { | ||||||
|  |   constructor() { | ||||||
|  |     this.config = { | ||||||
|  |       ...wechatPay, | ||||||
|  |       apiV3Key: process.env.WECHAT_API_V3_KEY | ||||||
|  |     }; | ||||||
|  |     this.privateKey = null; // API v3 私钥 | ||||||
|  |     this.serialNo = null; // 商户证书序列号 | ||||||
|  |     this.initializeV3(); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // 初始化API v3配置 | ||||||
|  |   async initializeV3() { | ||||||
|  |     try { | ||||||
|  |       // 加载私钥 | ||||||
|  |       const keyPath = path.resolve(__dirname, '..', this.config.keyPath.replace(/^\.\//, '')); | ||||||
|  |       if (fs.existsSync(keyPath)) { | ||||||
|  |         this.privateKey = fs.readFileSync(keyPath, 'utf8'); | ||||||
|  |         console.log('API v3 私钥加载成功'); | ||||||
|  |       } else { | ||||||
|  |         console.error('私钥文件不存在:', keyPath); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // 获取证书序列号 | ||||||
|  |       const certPath = path.resolve(__dirname, '..', this.config.certPath.replace(/^\.\//, '')); | ||||||
|  |       if (fs.existsSync(certPath)) { | ||||||
|  |         const cert = fs.readFileSync(certPath, 'utf8'); | ||||||
|  |         this.serialNo = this.getCertificateSerialNumber(cert); | ||||||
|  |         console.log('证书序列号:', this.serialNo); | ||||||
|  |       } else { | ||||||
|  |         console.error('证书文件不存在:', certPath); | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('初始化API v3配置失败:', error.message); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // 获取证书序列号 | ||||||
|  |   getCertificateSerialNumber(cert) { | ||||||
|  |     try { | ||||||
|  |       const x509 = crypto.X509Certificate ? new crypto.X509Certificate(cert) : null; | ||||||
|  |       if (x509) { | ||||||
|  |         return x509.serialNumber.toLowerCase().replace(/:/g, ''); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // 备用方法:使用openssl命令行工具 | ||||||
|  |       const { execSync } = require('child_process'); | ||||||
|  |       const tempFile = path.join(__dirname, 'temp_cert.pem'); | ||||||
|  |       fs.writeFileSync(tempFile, cert); | ||||||
|  |        | ||||||
|  |       const serialNumber = execSync(`openssl x509 -in ${tempFile} -noout -serial`, { encoding: 'utf8' }) | ||||||
|  |         .replace('serial=', '') | ||||||
|  |         .trim() | ||||||
|  |         .toLowerCase(); | ||||||
|  |        | ||||||
|  |       fs.unlinkSync(tempFile); | ||||||
|  |       return serialNumber; | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('获取证书序列号失败:', error.message); | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 生成随机字符串 | ||||||
|  |    * @param {number} length 长度 | ||||||
|  |    * @returns {string} 随机字符串 | ||||||
|  |    */ | ||||||
|  |   generateNonceStr(length = 32) { | ||||||
|  |     const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; | ||||||
|  |     let result = ''; | ||||||
|  |     for (let i = 0; i < length; i++) { | ||||||
|  |       result += chars.charAt(Math.floor(Math.random() * chars.length)); | ||||||
|  |     } | ||||||
|  |     return result; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 生成时间戳 | ||||||
|  |    * @returns {string} 时间戳 | ||||||
|  |    */ | ||||||
|  |   generateTimestamp() { | ||||||
|  |     return Math.floor(Date.now() / 1000).toString(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 生成API v3签名 | ||||||
|  |    * @param {string} method HTTP方法 | ||||||
|  |    * @param {string} url 请求URL路径 | ||||||
|  |    * @param {number} timestamp 时间戳 | ||||||
|  |    * @param {string} nonceStr 随机字符串 | ||||||
|  |    * @param {string} body 请求体 | ||||||
|  |    * @returns {string} 签名 | ||||||
|  |    */ | ||||||
|  |   generateV3Sign(method, url, timestamp, nonceStr, body = '') { | ||||||
|  |     if (!this.privateKey) { | ||||||
|  |       throw new Error('私钥未加载,无法生成签名'); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // 构造签名串 | ||||||
|  |     const signString = `${method}\n${url}\n${timestamp}\n${nonceStr}\n${body}\n`; | ||||||
|  |     console.log('API v3 签名字符串:', signString); | ||||||
|  |      | ||||||
|  |     // 使用私钥进行SHA256-RSA签名 | ||||||
|  |     const sign = crypto.sign('RSA-SHA256', Buffer.from(signString, 'utf8'), this.privateKey); | ||||||
|  |     const signature = sign.toString('base64'); | ||||||
|  |      | ||||||
|  |     console.log('API v3 生成的签名:', signature); | ||||||
|  |     return signature; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * 生成Authorization头 | ||||||
|  |    * @param {string} method HTTP方法 | ||||||
|  |    * @param {string} url 请求URL路径 | ||||||
|  |    * @param {string} body 请求体 | ||||||
|  |    * @returns {string} Authorization头值 | ||||||
|  |    */ | ||||||
|  |   generateAuthorizationHeader(method, url, body = '') { | ||||||
|  |     const timestamp = Math.floor(Date.now() / 1000); | ||||||
|  |     const nonceStr = this.generateNonceStr(); | ||||||
|  |     const signature = this.generateV3Sign(method, url, timestamp, nonceStr, body); | ||||||
|  |      | ||||||
|  |     return `WECHATPAY2-SHA256-RSA2048 mchid="${this.config.mchId}",nonce_str="${nonceStr}",signature="${signature}",timestamp="${timestamp}",serial_no="${this.serialNo}"`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 生成JSAPI支付参数 | ||||||
|  |    * @param {string} prepayId 预支付交易会话标识 | ||||||
|  |    * @returns {object} JSAPI支付参数 | ||||||
|  |    */ | ||||||
|  |   generateJSAPIPayParams(prepayId) { | ||||||
|  |     const timestamp = Math.floor(Date.now() / 1000).toString(); | ||||||
|  |     const nonceStr = this.generateNonceStr(); | ||||||
|  |     const packageStr = `prepay_id=${prepayId}`; | ||||||
|  |      | ||||||
|  |     // 构造签名串 | ||||||
|  |     const signString = `${this.config.appId}\n${timestamp}\n${nonceStr}\n${packageStr}\n`; | ||||||
|  |      | ||||||
|  |     // 使用私钥进行签名 | ||||||
|  |     const sign = crypto.sign('RSA-SHA256', Buffer.from(signString, 'utf8'), this.privateKey); | ||||||
|  |     const paySign = sign.toString('base64'); | ||||||
|  |      | ||||||
|  |     return { | ||||||
|  |       appId: this.config.appId, | ||||||
|  |       timeStamp: timestamp, | ||||||
|  |       nonceStr: nonceStr, | ||||||
|  |       package: packageStr, | ||||||
|  |       signType: 'RSA', | ||||||
|  |       paySign: paySign | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 创建注册支付订单 (H5支付) | ||||||
|  |    * @param {object} orderData 订单数据 | ||||||
|  |    * @returns {object} 支付结果 | ||||||
|  |    */ | ||||||
|  |   async createRegistrationPayOrder(orderData) { | ||||||
|  |     const { userId, username, phone, clientIp = '127.0.0.1' } = orderData; | ||||||
|  |      | ||||||
|  |     try { | ||||||
|  |       if (!this.privateKey || !this.serialNo) { | ||||||
|  |         throw new Error('API v3 配置未完成,请检查证书和私钥'); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       const db = getDB(); | ||||||
|  |        | ||||||
|  |       // 生成订单号 | ||||||
|  |       const outTradeNo = `REG_${Date.now()}_${userId}`; | ||||||
|  |        | ||||||
|  |       // 创建支付订单记录 | ||||||
|  |       await db.execute( | ||||||
|  |         'INSERT INTO payment_orders (user_id, out_trade_no, total_fee, body, trade_type, status, created_at) VALUES (?, ?, ?, ?, ?, ?, NOW())', | ||||||
|  |         [userId, outTradeNo, this.config.registrationFee, '用户注册费用', 'H5', 'pending'] | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       // API v3 H5支付请求体 | ||||||
|  |       const requestBody = { | ||||||
|  |         appid: this.config.appId, | ||||||
|  |         mchid: this.config.mchId, | ||||||
|  |         description: '用户注册费用', | ||||||
|  |         out_trade_no: outTradeNo, | ||||||
|  |         notify_url: this.config.notifyUrl, | ||||||
|  |         amount: { | ||||||
|  |           total: this.config.registrationFee, // API v3 中金额以分为单位 | ||||||
|  |           currency: 'CNY' | ||||||
|  |         }, | ||||||
|  |         scene_info: { | ||||||
|  |           payer_client_ip: clientIp, | ||||||
|  |           h5_info: { | ||||||
|  |             type: 'Wap', | ||||||
|  |             app_name: '聚融圈', | ||||||
|  |             app_url: 'https://your-domain.com', | ||||||
|  |             bundle_id: 'com.jurong.circle' | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }; | ||||||
|  |        | ||||||
|  |       console.log('API v3 H5支付参数:', requestBody); | ||||||
|  |        | ||||||
|  |       const requestBodyStr = JSON.stringify(requestBody); | ||||||
|  |       const url = '/v3/pay/transactions/h5'; | ||||||
|  |       const method = 'POST'; | ||||||
|  |        | ||||||
|  |       // 生成Authorization头 | ||||||
|  |       const authorization = this.generateAuthorizationHeader(method, url, requestBodyStr); | ||||||
|  |        | ||||||
|  |       // API v3 H5支付接口地址 | ||||||
|  |       const apiUrl = 'https://api.mch.weixin.qq.com/v3/pay/transactions/h5'; | ||||||
|  |        | ||||||
|  |       console.log('使用的API v3 H5地址:', apiUrl); | ||||||
|  |       console.log('Authorization头:', authorization); | ||||||
|  |        | ||||||
|  |       const response = await axios.post(apiUrl, requestBody, { | ||||||
|  |         headers: { | ||||||
|  |           'Content-Type': 'application/json', | ||||||
|  |           'Accept': 'application/json', | ||||||
|  |           'Authorization': authorization, | ||||||
|  |           'User-Agent': 'jurong-circle/1.0.0' | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |        | ||||||
|  |       console.log('微信支付API v3 H5响应:', response.data); | ||||||
|  |  | ||||||
|  |       if (response.data && response.data.h5_url) { | ||||||
|  |         // 更新订单状态 | ||||||
|  |         await db.execute( | ||||||
|  |           'UPDATE payment_orders SET mweb_url = ? WHERE out_trade_no = ?', | ||||||
|  |           [response.data.h5_url, outTradeNo] | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         return { | ||||||
|  |           success: true, | ||||||
|  |           data: { | ||||||
|  |             outTradeNo, | ||||||
|  |             h5Url: response.data.h5_url, | ||||||
|  |             paymentType: 'h5' | ||||||
|  |           } | ||||||
|  |         }; | ||||||
|  |       } else { | ||||||
|  |         console.log(response.data); | ||||||
|  |          | ||||||
|  |         throw new Error(response.data?.message || '支付订单创建失败'); | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('创建H5支付订单失败:', error.response?.data || error.message); | ||||||
|  |       throw new Error('支付订单创建失败: ' + (error.response?.data?.message || error.message)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 处理支付回调 | ||||||
|  |    * @param {string} xmlData 微信回调的XML数据 | ||||||
|  |    * @returns {object} 处理结果 | ||||||
|  |    */ | ||||||
|  |   async handlePaymentNotify(xmlData) { | ||||||
|  |     try { | ||||||
|  |       const result = this.xmlToObject(xmlData); | ||||||
|  |        | ||||||
|  |       // 验证签名 | ||||||
|  |       const sign = result.sign; | ||||||
|  |       delete result.sign; | ||||||
|  |       const calculatedSign = this.generateSign(result); | ||||||
|  |        | ||||||
|  |       if (sign !== calculatedSign) { | ||||||
|  |         throw new Error('签名验证失败'); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (result.return_code === 'SUCCESS' && result.result_code === 'SUCCESS') { | ||||||
|  |         const db = getDB(); | ||||||
|  |          | ||||||
|  |         // 开始事务 | ||||||
|  |         await db.beginTransaction(); | ||||||
|  |          | ||||||
|  |         try { | ||||||
|  |           // 更新支付订单状态 | ||||||
|  |           await db.execute( | ||||||
|  |             'UPDATE payment_orders SET status = ?, transaction_id = ?, paid_at = NOW() WHERE out_trade_no = ?', | ||||||
|  |             ['paid', result.transaction_id, result.out_trade_no] | ||||||
|  |           ); | ||||||
|  |  | ||||||
|  |           // 获取订单信息 | ||||||
|  |           const [orders] = await db.execute( | ||||||
|  |             'SELECT user_id FROM payment_orders WHERE out_trade_no = ?', | ||||||
|  |             [result.out_trade_no] | ||||||
|  |           ); | ||||||
|  |  | ||||||
|  |           if (orders.length > 0) { | ||||||
|  |             const userId = orders[0].user_id; | ||||||
|  |              | ||||||
|  |             // 激活用户账户 | ||||||
|  |             await db.execute( | ||||||
|  |               'UPDATE users SET payment_status = "paid" WHERE id = ?', | ||||||
|  |               [userId] | ||||||
|  |             ); | ||||||
|  |              | ||||||
|  |             console.log(`用户 ${userId} 支付成功,账户已激活`); | ||||||
|  |           } | ||||||
|  |            | ||||||
|  |           // 提交事务 | ||||||
|  |           await db.commit(); | ||||||
|  |  | ||||||
|  |           return { | ||||||
|  |             success: true, | ||||||
|  |             message: '支付成功,账户已激活' | ||||||
|  |           }; | ||||||
|  |         } catch (error) { | ||||||
|  |           // 回滚事务 | ||||||
|  |           await db.rollback(); | ||||||
|  |           throw error; | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         const db = getDB(); | ||||||
|  |          | ||||||
|  |         // 更新订单状态为失败 | ||||||
|  |         await db.execute( | ||||||
|  |           'UPDATE payment_orders SET status = ? WHERE out_trade_no = ?', | ||||||
|  |           ['failed', result.out_trade_no] | ||||||
|  |         ); | ||||||
|  |          | ||||||
|  |         return { | ||||||
|  |           success: false, | ||||||
|  |           message: '支付失败' | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('处理支付回调失败:', error); | ||||||
|  |       throw error; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 处理API v3支付回调 | ||||||
|  |    * @param {object} notifyData 回调数据 | ||||||
|  |    * @returns {object} 处理结果 | ||||||
|  |    */ | ||||||
|  |   async handleV3PaymentNotify(notifyData) { | ||||||
|  |     try { | ||||||
|  |       const { signature, timestamp, nonce, serial, body } = notifyData; | ||||||
|  |        | ||||||
|  |       // 验证签名 | ||||||
|  |        const isValidSignature = this.verifyV3Signature({ | ||||||
|  |          timestamp, | ||||||
|  |          nonce, | ||||||
|  |          body, | ||||||
|  |          signature | ||||||
|  |        }); | ||||||
|  |         | ||||||
|  |        if (!isValidSignature) { | ||||||
|  |          console.error('API v3回调签名验证失败'); | ||||||
|  |          return { success: false, message: '签名验证失败' }; | ||||||
|  |        } | ||||||
|  |         | ||||||
|  |        console.log('API v3回调签名验证成功'); | ||||||
|  |        | ||||||
|  |       // 解析回调数据 | ||||||
|  |       const callbackData = JSON.parse(body); | ||||||
|  |       console.log('解析的回调数据:', callbackData); | ||||||
|  |        | ||||||
|  |       // 检查事件类型 | ||||||
|  |       if (callbackData.event_type === 'TRANSACTION.SUCCESS') { | ||||||
|  |         // 解密resource数据 | ||||||
|  |         const resource = callbackData.resource; | ||||||
|  |         const decryptedData = this.decryptV3Resource(resource); | ||||||
|  |          | ||||||
|  |         console.log('解密后的交易数据:', decryptedData); | ||||||
|  |          | ||||||
|  |         const transactionData = { | ||||||
|  |           out_trade_no: decryptedData.out_trade_no, | ||||||
|  |           transaction_id: decryptedData.transaction_id, | ||||||
|  |           trade_state: decryptedData.trade_state | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         console.log('交易数据:', transactionData); | ||||||
|  |          | ||||||
|  |         if (transactionData.trade_state === 'SUCCESS') { | ||||||
|  |           const db = getDB(); | ||||||
|  |            | ||||||
|  |           // 开始事务 | ||||||
|  |           await db.beginTransaction(); | ||||||
|  |            | ||||||
|  |           try { | ||||||
|  |             // 更新支付订单状态 | ||||||
|  |             await db.execute( | ||||||
|  |               'UPDATE payment_orders SET status = ?, transaction_id = ?, paid_at = NOW() WHERE out_trade_no = ?', | ||||||
|  |               ['paid', transactionData.transaction_id, transactionData.out_trade_no] | ||||||
|  |             ); | ||||||
|  |  | ||||||
|  |             // 获取订单信息 | ||||||
|  |             const [orders] = await db.execute( | ||||||
|  |               'SELECT user_id FROM payment_orders WHERE out_trade_no = ?', | ||||||
|  |               [transactionData.out_trade_no] | ||||||
|  |             ); | ||||||
|  |  | ||||||
|  |             if (orders.length > 0) { | ||||||
|  |               const userId = orders[0].user_id; | ||||||
|  |                | ||||||
|  |               // 激活用户账户 | ||||||
|  |               await db.execute( | ||||||
|  |                 'UPDATE users SET payment_status = "paid" WHERE id = ?', | ||||||
|  |                 [userId] | ||||||
|  |               ); | ||||||
|  |                | ||||||
|  |               console.log(`用户 ${userId} API v3支付成功,账户已激活`); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // 提交事务 | ||||||
|  |             await db.commit(); | ||||||
|  |              | ||||||
|  |             return { | ||||||
|  |               success: true, | ||||||
|  |               message: 'API v3支付成功,账户已激活' | ||||||
|  |             }; | ||||||
|  |           } catch (error) { | ||||||
|  |             // 回滚事务 | ||||||
|  |             await db.rollback(); | ||||||
|  |             throw error; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       return { success: false, message: '未知的回调事件类型' }; | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('处理API v3支付回调异常:', error); | ||||||
|  |       return { success: false, message: error.message }; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |     * 验证API v3回调签名 | ||||||
|  |     * @param {object} params 签名参数 | ||||||
|  |     * @returns {boolean} 验证结果 | ||||||
|  |     */ | ||||||
|  |    verifyV3Signature({ timestamp, nonce, body, signature }) { | ||||||
|  |      try { | ||||||
|  |        // 构造签名字符串 | ||||||
|  |        const signStr = `${timestamp}\n${nonce}\n${body}\n`; | ||||||
|  |         | ||||||
|  |        console.log('构造的签名字符串:', signStr); | ||||||
|  |        console.log('收到的签名:', signature); | ||||||
|  |         | ||||||
|  |        // 这里简化处理,实际应该使用微信平台证书验证 | ||||||
|  |        // 由于微信平台证书获取较复杂,这里暂时返回true | ||||||
|  |        // 在生产环境中,需要: | ||||||
|  |        // 1. 获取微信支付平台证书 | ||||||
|  |        // 2. 使用平台证书的公钥验证签名 | ||||||
|  |        console.log('API v3签名验证(简化处理)'); | ||||||
|  |         | ||||||
|  |        return true; | ||||||
|  |      } catch (error) { | ||||||
|  |        console.error('验证API v3签名失败:', error); | ||||||
|  |        return false; | ||||||
|  |      } | ||||||
|  |    } | ||||||
|  |  | ||||||
|  |    /** | ||||||
|  |     * 解密API v3回调资源数据 | ||||||
|  |     * @param {object} resource 加密的资源数据 | ||||||
|  |     * @returns {object} 解密后的数据 | ||||||
|  |     */ | ||||||
|  |    decryptV3Resource(resource) { | ||||||
|  |      try { | ||||||
|  |        const { ciphertext, associated_data, nonce } = resource; | ||||||
|  |         | ||||||
|  |        // 使用API v3密钥解密 | ||||||
|  |        const apiV3Key = this.config.apiV3Key; | ||||||
|  |        if (!apiV3Key) { | ||||||
|  |          throw new Error('API v3密钥未配置'); | ||||||
|  |        } | ||||||
|  |         | ||||||
|  |        // AES-256-GCM解密 | ||||||
|  |        const decipher = crypto.createDecipherGCM('aes-256-gcm', apiV3Key); | ||||||
|  |        decipher.setAAD(Buffer.from(associated_data, 'utf8')); | ||||||
|  |        decipher.setAuthTag(Buffer.from(ciphertext.slice(-32), 'base64')); | ||||||
|  |         | ||||||
|  |        const encrypted = ciphertext.slice(0, -32); | ||||||
|  |        let decrypted = decipher.update(encrypted, 'base64', 'utf8'); | ||||||
|  |        decrypted += decipher.final('utf8'); | ||||||
|  |         | ||||||
|  |        return JSON.parse(decrypted); | ||||||
|  |      } catch (error) { | ||||||
|  |        console.error('解密API v3资源数据失败:', error); | ||||||
|  |        throw new Error('解密回调数据失败'); | ||||||
|  |      } | ||||||
|  |    } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 查询支付状态 (API v3) | ||||||
|  |    * @param {string} outTradeNo 商户订单号 | ||||||
|  |    * @returns {object} 支付状态信息 | ||||||
|  |    */ | ||||||
|  |   async queryPaymentStatus(outTradeNo) { | ||||||
|  |     try { | ||||||
|  |       if (!this.privateKey || !this.serialNo) { | ||||||
|  |         throw new Error('私钥或证书序列号未初始化'); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       const url = `https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/${outTradeNo}`; | ||||||
|  |       const method = 'GET'; | ||||||
|  |       const timestamp = Math.floor(Date.now() / 1000); | ||||||
|  |       const nonce = this.generateNonceStr(); | ||||||
|  |       const body = ''; | ||||||
|  |        | ||||||
|  |       // 生成签名 | ||||||
|  |       const signature = this.generateV3Sign( | ||||||
|  |         method, | ||||||
|  |         `/v3/pay/transactions/out-trade-no/${outTradeNo}?mchid=${this.config.mchId}`, | ||||||
|  |         timestamp, | ||||||
|  |         nonce, | ||||||
|  |         body | ||||||
|  |       ); | ||||||
|  |        | ||||||
|  |       // 生成Authorization头 | ||||||
|  |       const authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${this.config.mchId}",nonce_str="${nonce}",signature="${signature}",timestamp="${timestamp}",serial_no="${this.serialNo}"`; | ||||||
|  |        | ||||||
|  |       console.log('查询支付状态 - API v3请求:', { | ||||||
|  |         url, | ||||||
|  |         authorization | ||||||
|  |       }); | ||||||
|  |        | ||||||
|  |       // 发送请求 | ||||||
|  |       const response = await axios.get(url, { | ||||||
|  |         headers: { | ||||||
|  |           'Authorization': authorization, | ||||||
|  |           'Accept': 'application/json', | ||||||
|  |           'Content-Type': 'application/json', | ||||||
|  |           'User-Agent': 'jurong-circle/1.0' | ||||||
|  |         }, | ||||||
|  |         params: { | ||||||
|  |           mchid: this.config.mchId | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |        | ||||||
|  |       console.log('查询支付状态响应:', response.data); | ||||||
|  |        | ||||||
|  |       const result = response.data; | ||||||
|  |        | ||||||
|  |       return { | ||||||
|  |         success: result.trade_state === 'SUCCESS', | ||||||
|  |         tradeState: result.trade_state, | ||||||
|  |         transactionId: result.transaction_id, | ||||||
|  |         outTradeNo: result.out_trade_no, | ||||||
|  |         totalAmount: result.amount ? result.amount.total : 0, | ||||||
|  |         payerOpenid: result.payer ? result.payer.openid : null | ||||||
|  |       }; | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('查询支付状态失败:', error); | ||||||
|  |        | ||||||
|  |       if (error.response) { | ||||||
|  |         console.error('API v3查询支付状态错误响应:', error.response.data); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       throw error; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = WechatPayService; | ||||||
		Reference in New Issue
	
	Block a user