commit 95c66c0a8afe9154439713682ac12fc97ae1c72b Author: shanshanzhong Date: Fri Oct 10 07:13:36 2025 -0700 init diff --git a/.commitlintrc.json b/.commitlintrc.json new file mode 100644 index 0000000..c30e5a9 --- /dev/null +++ b/.commitlintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["@commitlint/config-conventional"] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb39815 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.idea/ +.vscode/ +*-dev.yaml +*.local.yaml +/test/ +*.log +.DS_Store +*_test_config.go +/build/ +etc/ppanel.yaml +*.p8 +*.crt +*.key +node_modules +package-lock.json +package.json \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..fa6d223 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,66 @@ +project_name: ppanel +version: 1 +release: + prerelease: auto +builds: + - # If true, skip the build. + # Useful for library projects. + # Default is false + skip: true + +changelog: + # Set it to true if you wish to skip the changelog generation. + # This may result in an empty release notes on GitHub/GitLab/Gitea. + disable: false + + # Changelog generation implementation to use. + # + # Valid options are: + # - `git`: uses `git log`; + # - `github`: uses the compare GitHub API, appending the author login to the changelog. + # - `gitlab`: uses the compare GitLab API, appending the author name and email to the changelog. + # - `github-native`: uses the GitHub release notes generation API, disables the groups feature. + # + # Defaults to `git`. + use: github + + # Sorts the changelog by the commit's messages. + # Could either be asc, desc or empty + # Default is empty + sort: asc + + # Format to use for commit formatting. + # Only available when use is one of `github`, `gitea`, or `gitlab`. + # + # Default: '{{ .SHA }}: {{ .Message }} ({{ with .AuthorUsername }}@{{ . }}{{ else }}{{ .AuthorName }} <{{ .AuthorEmail }}>{{ end }})'. + # Extra template fields: `SHA`, `Message`, `AuthorName`, `AuthorEmail`, and + # `AuthorUsername`. + format: "{{ .Message }}" + + # Group commits messages by given regex and title. + # Order value defines the order of the groups. + # Proving no regex means all commits will be grouped under the default group. + # Groups are disabled when using github-native, as it already groups things by itself. + # + # Default is no groups. + groups: + - title: "✨ Features" + regexp: "^.*feat[(\\w)]*:+.*$" + order: 0 + - title: "🐛 Bug Fixes" + regexp: "^.*fix[(\\w)]*:+.*$" + order: 1 + - title: "🎫 Chores" + regexp: "^.*chore[(\\w)]*:+.*$" + order: 2 + - title: "🔨 Refactor" + regexp: "^.*refactor[(\\w)]*:+.*$" + order: 3 + - title: "🔧 Build" + regexp: "^.*?(ci)(\\(.+\\))??!?:.+$" + order: 4 + - title: "📝 Documentation" + regexp: "^.*?docs?(\\(.+\\))??!?:.+$" + order: 5 + - title: "✨ Others" + order: 999 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..063be39 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,40 @@ +# Pull Request Submission Guidelines + +To ensure the quality of the codebase and maintainability of the project, please follow these guidelines before submitting a Pull Request (PR): + +## 1. PR Title and Description + +- **Clear Title**: Concisely describe the main content of the PR, for example: + - Fix: Correct error messages in user login + - Feature: Add order export functionality + +- **Detailed Description**: Include the following details in the description: + - Purpose and background of this PR. + - Detailed explanation of the changes. + - For bug fixes, describe the steps to reproduce the issue. + - For new features, explain how to use them. + - Link related issues (if any) using keywords like `Closes #123`. + +## 2. Code Checks Before Submission + +- **Code Style**: Ensure the code adheres to the project's coding standards (e.g., ESLint, Prettier, or GoLint). +- **Functional Testing**: Fully test new features or bug fixes to ensure no missing functionality or regressions. +- **Unit Tests**: Write unit tests for added or modified functionality and ensure all tests pass. +- **Documentation Updates**: Update documentation if the PR includes new features or API changes. + +## 3. Branch Strategy + +- **Correct Branch**: + - Develop new features based on `feature/*` branches. + - Fix bugs based on `fix/*` branches. + - Ensure the target branch of the PR aligns with the project's branching strategy. + +- **Sync with Base Branch**: Before submitting the PR, ensure your branch is up-to-date with the target branch (e.g., `main` or `develop`). + +## 4. Review Process + +- **Small Commits**: Avoid submitting excessive changes in a single PR; break it into smaller logical units. + +--- + +Thank you for your contribution! diff --git a/CONTRIBUTING_ZH.md b/CONTRIBUTING_ZH.md new file mode 100644 index 0000000..c27158e --- /dev/null +++ b/CONTRIBUTING_ZH.md @@ -0,0 +1,44 @@ +# Pull Request 提交须知 + +为了确保代码库的质量和项目的可维护性,在提交 Pull Request(PR)之前,请务必遵循以下准则: + +## 1. PR 标题和描述 + +- **标题清晰**:简明扼要地描述 PR 的主要内容,例如: + - Fix: 修复用户登录时的错误提示 + - Feature: 添加订单导出功能 + +- **描述详细**:在描述中包括以下内容: + - 此 PR 的目的和背景。 + - 变更的详细说明。 + - 如果涉及 Bug 修复,需描述问题重现的步骤。 + - 如果涉及新功能,需描述其使用方式。 + - 关联的 Issue(如有),使用关键字关闭 Issue,例如:`Closes #123`。 + +## 2. 提交代码前的检查 + +- **代码风格**:确保代码符合项目的代码规范。 +- **功能测试**:对新功能或 Bug 修复进行全面测试,确保没有功能缺失或回归问题。 +- **单元测试**:为新增或修改的功能编写单元测试,并确保所有测试通过(工具类即`pkg/*`下面的必须带有单元测试)。 +- **文档更新**:如果 PR 涉及新功能或接口更改,确保文档同步更新。 + +## 3. 分支策略 + +- **正确的分支**: + - 新功能应基于 `feature/*` 分支进行开发。 + - Bug 修复应基于 `fix/*` 分支。 + - 确保 PR 的目标分支与项目的分支策略一致。 + +- **同步主干代码**:在提交 PR 之前,请确保分支已经与目标分支(develop)同步。 + +## 4. 审查流程 + +- **小型提交**:避免一次性提交过多的更改,将 PR 拆分为更小的逻辑单元。 + +--- + +感谢您的贡献! + + + + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6aabce2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,44 @@ +# Use a smaller base image for the build stage +FROM golang:alpine AS builder + +LABEL stage=gobuilder + +ARG TARGETARCH +ENV CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} + +# Combine apk commands into one to reduce layer size +RUN apk update --no-cache && apk add --no-cache tzdata ca-certificates + +WORKDIR /build + +# Copy go.mod and go.sum first to take advantage of Docker caching +COPY go.mod go.sum ./ +RUN go mod download + +# Copy the rest of the application code +COPY . . + +# Build the binary with optimization flags to reduce binary size +RUN go build -ldflags="-s -w" -o /app/ppanel ppanel.go + +# Final minimal image +FROM scratch + +# Copy CA certificates and timezone data +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /usr/share/zoneinfo/Asia/Shanghai + +ENV TZ=Asia/Shanghai + +# Set working directory and copy binary +WORKDIR /app + +COPY --from=builder /app/ppanel /app/ppanel +COPY --from=builder /etc /app/etc + +# Expose the port (optional) +EXPOSE 8080 + +# Specify entry point +ENTRYPOINT ["/app/ppanel"] +CMD ["run", "--config", "etc/ppanel.yaml"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e72bfdd --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/apis/admin/ads.api b/apis/admin/ads.api new file mode 100644 index 0000000..1fe750c --- /dev/null +++ b/apis/admin/ads.api @@ -0,0 +1,79 @@ +syntax = "v1" + +info ( + title: "Ads API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +type ( + CreateAdsRequest { + Title string `json:"title"` + Type string `json:"type"` + Content string `json:"content"` + Description string `json:"description"` + TargetURL string `json:"target_url"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + Status int `json:"status"` + } + UpdateAdsRequest { + Id int64 `json:"id"` + Title string `json:"title"` + Type string `json:"type"` + Content string `json:"content"` + Description string `json:"description"` + TargetURL string `json:"target_url"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + Status int `json:"status"` + } + DeleteAdsRequest { + Id int64 `json:"id"` + } + GetAdsListRequest { + Page int `form:"page"` + Size int `form:"size"` + Status *int `form:"status,omitempty"` + Search string `form:"search,omitempty"` + } + GetAdsListResponse { + Total int64 `json:"total"` + List []Ads `json:"list"` + } + GetAdsDetailRequest { + Id int64 `form:"id"` + } +) + +import "../types.api" + +@server ( + prefix: v1/admin/ads + group: admin/ads + middleware: AuthMiddleware +) +service ppanel { + @doc "Create Ads" + @handler CreateAds + post / (CreateAdsRequest) + + @doc "Update Ads" + @handler UpdateAds + put / (UpdateAdsRequest) + + @doc "Delete Ads" + @handler DeleteAds + delete / (DeleteAdsRequest) + + @doc "Get Ads List" + @handler GetAdsList + get /list (GetAdsListRequest) returns (GetAdsListResponse) + + @doc "Get Ads Detail" + @handler GetAdsDetail + get /detail (GetAdsDetailRequest) returns (Ads) +} + diff --git a/apis/admin/announcement.api b/apis/admin/announcement.api new file mode 100644 index 0000000..03d7082 --- /dev/null +++ b/apis/admin/announcement.api @@ -0,0 +1,76 @@ +syntax = "v1" + +info ( + title: "Announcement API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +type ( + CreateAnnouncementRequest { + Title string `json:"title" validate:"required"` + Content string `json:"content" validate:"required"` + } + UpdateAnnouncementRequest { + Id int64 `json:"id" validate:"required"` + Title string `json:"title"` + Content string `json:"content"` + Show *bool `json:"show"` + Pinned *bool `json:"pinned"` + Popup *bool `json:"popup"` + } + UpdateAnnouncementEnableRequest { + Id int64 `json:"id" validate:"required"` + Enable *bool `json:"enable" validate:"required"` + } + DeleteAnnouncementRequest { + Id int64 `json:"id" validate:"required"` + } + GetAnnouncementListRequest { + Page int64 `form:"page"` + Size int64 `form:"size"` + Show *bool `form:"show,omitempty"` + Pinned *bool `form:"pinned,omitempty"` + Popup *bool `form:"popup,omitempty"` + Search string `form:"search,omitempty"` + } + GetAnnouncementListResponse { + Total int64 `json:"total"` + List []Announcement `json:"list"` + } + GetAnnouncementRequest { + Id int64 `form:"id" validate:"required"` + } +) + +@server ( + prefix: v1/admin/announcement + group: admin/announcement + middleware: AuthMiddleware +) +service ppanel { + @doc "Create announcement" + @handler CreateAnnouncement + post / (CreateAnnouncementRequest) + + @doc "Update announcement" + @handler UpdateAnnouncement + put / (UpdateAnnouncementRequest) + + @doc "Get announcement list" + @handler GetAnnouncementList + get /list (GetAnnouncementListRequest) returns (GetAnnouncementListResponse) + + @doc "Delete announcement" + @handler DeleteAnnouncement + delete / (DeleteAnnouncementRequest) + + @doc "Get announcement" + @handler GetAnnouncement + get /detail (GetAnnouncementRequest) returns (Announcement) +} + diff --git a/apis/admin/auth.api b/apis/admin/auth.api new file mode 100644 index 0000000..4161cb5 --- /dev/null +++ b/apis/admin/auth.api @@ -0,0 +1,76 @@ +syntax = "v1" + +info ( + title: "Auth Method Management" + desc: "System auth method management" + author: "Tension" + email: "tension@ppanel.com" + version: "0.1.2" +) + +import ( + "../types.api" +) + +type ( + UpdateAuthMethodConfigRequest { + Id int64 `json:"id"` + Method string `json:"method"` + Config interface{} `json:"config"` + Enabled *bool `json:"enabled"` + } + GetAuthMethodConfigRequest { + Method string `form:"method"` + } + GetAuthMethodListResponse { + List []AuthMethodConfig `json:"list"` + } + + + TestSmsSendRequest { + AreaCode string `json:"area_code" validate:"required"` + Telephone string `json:"telephone" validate:"required"` + } + // Test email smtp request + TestEmailSendRequest { + Email string `json:"email" validate:"required"` + } + +) + +@server ( + prefix: v1/admin/auth-method + group: admin/authMethod + middleware: AuthMiddleware +) +service ppanel { + @doc "Get auth method list" + @handler GetAuthMethodList + get /list returns (GetAuthMethodListResponse) + + @doc "Get auth method config" + @handler GetAuthMethodConfig + get /config (GetAuthMethodConfigRequest) returns (AuthMethodConfig) + + @doc "Update auth method config" + @handler UpdateAuthMethodConfig + put /config (UpdateAuthMethodConfigRequest) returns (AuthMethodConfig) + + + @doc "Test sms send" + @handler TestSmsSend + post /test_sms_send (TestSmsSendRequest) + + @doc "Test email send" + @handler TestEmailSend + post /test_email_send (TestEmailSendRequest) + + @doc "Get sms support platform" + @handler GetSmsPlatform + get /sms_platform returns (PlatformResponse) + + @doc "Get email support platform" + @handler GetEmailPlatform + get /email_platform returns (PlatformResponse) +} + diff --git a/apis/admin/console.api b/apis/admin/console.api new file mode 100644 index 0000000..c20bb14 --- /dev/null +++ b/apis/admin/console.api @@ -0,0 +1,88 @@ +syntax = "v1" + +info ( + title: "Console API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +type ( + ServerTrafficData { + ServerId int64 `json:"server_id"` + Name string `json:"name"` + Upload int64 `json:"upload"` + Download int64 `json:"download"` + } + UserTrafficData { + SID int64 `json:"sid"` + Upload int64 `json:"upload"` + Download int64 `json:"download"` + } + ServerTotalDataResponse { + OnlineUserIPs int64 `json:"online_user_ips"` + OnlineServers int64 `json:"online_servers"` + OfflineServers int64 `json:"offline_servers"` + TodayUpload int64 `json:"today_upload"` + TodayDownload int64 `json:"today_download"` + MonthlyUpload int64 `json:"monthly_upload"` + MonthlyDownload int64 `json:"monthly_download"` + UpdatedAt int64 `json:"updated_at"` + ServerTrafficRankingToday []ServerTrafficData `json:"server_traffic_ranking_today"` + ServerTrafficRankingYesterday []ServerTrafficData `json:"server_traffic_ranking_yesterday"` + UserTrafficRankingToday []UserTrafficData `json:"user_traffic_ranking_today"` + UserTrafficRankingYesterday []UserTrafficData `json:"user_traffic_ranking_yesterday"` + } + UserStatistics { + Date string `json:"date,omitempty"` + Register int64 `json:"register"` + NewOrderUsers int64 `json:"new_order_users"` + RenewalOrderUsers int64 `json:"renewal_order_users"` + List []UserStatistics `json:"list,omitempty"` + } + OrdersStatistics { + Date string `json:"date,omitempty"` + AmountTotal int64 `json:"amount_total"` + NewOrderAmount int64 `json:"new_order_amount"` + RenewalOrderAmount int64 `json:"renewal_order_amount"` + List []OrdersStatistics `json:"list,omitempty"` + } + RevenueStatisticsResponse { + Today OrdersStatistics `json:"today"` + Monthly OrdersStatistics `json:"monthly"` + All OrdersStatistics `json:"all"` + } + UserStatisticsResponse { + Today UserStatistics `json:"today"` + Monthly UserStatistics `json:"monthly"` + All UserStatistics `json:"all"` + } + TicketWaitRelpyResponse { + Count int64 `json:"count"` + } +) + +@server ( + prefix: v1/admin/console + group: admin/console + middleware: AuthMiddleware +) +service ppanel { + @doc "Query server total data" + @handler QueryServerTotalData + get /server returns (ServerTotalDataResponse) + + @doc "Query revenue statistics" + @handler QueryRevenueStatistics + get /revenue returns (RevenueStatisticsResponse) + + @doc "Query user statistics" + @handler QueryUserStatistics + get /user returns (UserStatisticsResponse) + + @doc "Query ticket wait reply" + @handler QueryTicketWaitReply + get /ticket returns (TicketWaitRelpyResponse) +} + diff --git a/apis/admin/coupon.api b/apis/admin/coupon.api new file mode 100644 index 0000000..92516aa --- /dev/null +++ b/apis/admin/coupon.api @@ -0,0 +1,85 @@ +syntax = "v1" + +info ( + title: "coupon API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +type ( + CreateCouponRequest { + Name string `json:"name" validate:"required"` + Code string `json:"code,omitempty"` + Count int64 `json:"count,omitempty"` + Type uint8 `json:"type" validate:"required"` + Discount int64 `json:"discount" validate:"required"` + StartTime int64 `json:"start_time" validate:"required"` + ExpireTime int64 `json:"expire_time" validate:"required"` + UserLimit int64 `json:"user_limit,omitempty"` + Subscribe []int64 `json:"subscribe,omitempty"` + UsedCount int64 `json:"used_count,omitempty"` + Enable *bool `json:"enable,omitempty"` + } + UpdateCouponRequest { + Id int64 `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` + Code string `json:"code,omitempty"` + Count int64 `json:"count,omitempty"` + Type uint8 `json:"type" validate:"required"` + Discount int64 `json:"discount" validate:"required"` + StartTime int64 `json:"start_time" validate:"required"` + ExpireTime int64 `json:"expire_time" validate:"required"` + UserLimit int64 `json:"user_limit,omitempty"` + Subscribe []int64 `json:"subscribe,omitempty"` + UsedCount int64 `json:"used_count,omitempty"` + Enable *bool `json:"enable,omitempty"` + } + DeleteCouponRequest { + Id int64 `json:"id" validate:"required"` + } + BatchDeleteCouponRequest { + Ids []int64 `json:"ids" validate:"required"` + } + GetCouponListRequest { + Page int64 `form:"page" validate:"required"` + Size int64 `form:"size" validate:"required"` + Subscribe int64 `form:"subscribe,omitempty"` + Search string `form:"search,omitempty"` + } + GetCouponListResponse { + Total int64 `json:"total"` + List []Coupon `json:"list"` + } +) + +@server ( + prefix: v1/admin/coupon + group: admin/coupon + middleware: AuthMiddleware +) +service ppanel { + @doc "Create coupon" + @handler CreateCoupon + post / (CreateCouponRequest) + + @doc "Update coupon" + @handler UpdateCoupon + put / (UpdateCouponRequest) + + @doc "Delete coupon" + @handler DeleteCoupon + delete / (DeleteCouponRequest) + + @doc "Batch delete coupon" + @handler BatchDeleteCoupon + delete /batch (BatchDeleteCouponRequest) + + @doc "Get coupon list" + @handler GetCouponList + get /list (GetCouponListRequest) returns (GetCouponListResponse) +} + diff --git a/apis/admin/device.api b/apis/admin/device.api new file mode 100644 index 0000000..1e5fb59 --- /dev/null +++ b/apis/admin/device.api @@ -0,0 +1,13 @@ +syntax = "v1" + +info( + title: "Device API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +type ( + +) \ No newline at end of file diff --git a/apis/admin/document.api b/apis/admin/document.api new file mode 100644 index 0000000..be5440a --- /dev/null +++ b/apis/admin/document.api @@ -0,0 +1,78 @@ +syntax = "v1" + +info ( + title: "Document API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +type ( + CreateDocumentRequest { + Title string `json:"title" validate:"required"` + Content string `json:"content" validate:"required"` + Tags []string `json:"tags,omitempty" ` + Show *bool `json:"show"` + } + UpdateDocumentRequest { + Id int64 `json:"id" validate:"required"` + Title string `json:"title" validate:"required"` + Content string `json:"content" validate:"required"` + Tags []string `json:"tags,omitempty" ` + Show *bool `json:"show"` + } + DeleteDocumentRequest { + Id int64 `json:"id" validate:"required"` + } + BatchDeleteDocumentRequest { + Ids []int64 `json:"ids" validate:"required"` + } + GetDocumentListRequest { + Page int64 `form:"page" validate:"required"` + Size int64 `form:"size" validate:"required"` + Tag string `form:"tag,omitempty"` + Search string `form:"search,omitempty"` + } + GetDocumentListResponse { + Total int64 `json:"total"` + List []Document `json:"list"` + } + GetDocumentDetailRequest { + Id int64 `json:"id" validate:"required"` + } +) + +@server ( + prefix: v1/admin/document + group: admin/document + middleware: AuthMiddleware +) +service ppanel { + @doc "Create document" + @handler CreateDocument + post / (CreateDocumentRequest) + + @doc "Update document" + @handler UpdateDocument + put / (UpdateDocumentRequest) + + @doc "Delete document" + @handler DeleteDocument + delete / (DeleteDocumentRequest) + + @doc "Batch delete document" + @handler BatchDeleteDocument + delete /batch (BatchDeleteDocumentRequest) + + @doc "Get document list" + @handler GetDocumentList + get /list (GetDocumentListRequest) returns (GetDocumentListResponse) + + @doc "Get document detail" + @handler GetDocumentDetail + get /detail (GetDocumentDetailRequest) returns (Document) +} + diff --git a/apis/admin/log.api b/apis/admin/log.api new file mode 100644 index 0000000..12fc312 --- /dev/null +++ b/apis/admin/log.api @@ -0,0 +1,40 @@ +syntax = "v1" + +info ( + title: "Log API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +type ( + GetMessageLogListRequest { + Page int `form:"page"` + Size int `form:"size"` + Type string `form:"type"` + Platform string `form:"platform,omitempty"` + To string `form:"to,omitempty"` + Subject string `form:"subject,omitempty"` + Content string `form:"content,omitempty"` + Status int `form:"status,omitempty"` + } + GetMessageLogListResponse { + Total int64 `json:"total"` + List []MessageLog `json:"list"` + } +) + +@server ( + prefix: v1/admin/log + group: admin/log + middleware: AuthMiddleware +) +service ppanel { + @doc "Get message log list" + @handler GetMessageLogList + get /message/list (GetMessageLogListRequest) returns (GetMessageLogListResponse) +} + diff --git a/apis/admin/order.api b/apis/admin/order.api new file mode 100644 index 0000000..0d49074 --- /dev/null +++ b/apis/admin/order.api @@ -0,0 +1,68 @@ +syntax = "v1" + +info ( + title: "order API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +type ( + CreateOrderRequest { + UserId int64 `json:"user_id" validate:"required"` + Type uint8 `json:"type" validate:"required"` + Quantity int64 `json:"quantity,omitempty"` + Price int64 `json:"price" validate:"required"` + Amount int64 `json:"amount" validate:"required"` + Discount int64 `json:"discount,omitempty"` + Coupon string `json:"coupon,omitempty"` + CouponDiscount int64 `json:"coupon_discount,omitempty"` + Commission int64 `json:"commission"` + FeeAmount int64 `json:"fee_amount" validate:"required"` + PaymentId int64 `json:"payment_id" validate:"required"` + TradeNo string `json:"trade_no,omitempty"` + Status uint8 `json:"status,omitempty"` + SubscribeId int64 `json:"subscribe_id,omitempty"` + } + UpdateOrderStatusRequest { + Id int64 `json:"id" validate:"required"` + Status uint8 `json:"status" validate:"required"` + PaymentId int64 `json:"payment_id,omitempty"` + TradeNo string `json:"trade_no,omitempty"` + } + GetOrderListRequest { + Page int64 `form:"page" validate:"required"` + Size int64 `form:"size" validate:"required"` + UserId int64 `form:"user_id,omitempty"` + Status uint8 `form:"status,omitempty"` + SubscribeId int64 `form:"subscribe_id,omitempty"` + Search string `form:"search,omitempty"` + } + GetOrderListResponse { + Total int64 `json:"total"` + List []Order `json:"list"` + } +) + +@server ( + prefix: v1/admin/order + group: admin/order + middleware: AuthMiddleware +) +service ppanel { + @doc "Create order" + @handler CreateOrder + post / (CreateOrderRequest) + + @doc "Get order list" + @handler GetOrderList + get /list (GetOrderListRequest) returns (GetOrderListResponse) + + @doc "Update order status" + @handler UpdateOrderStatus + put /status (UpdateOrderStatusRequest) +} + diff --git a/apis/admin/payment.api b/apis/admin/payment.api new file mode 100644 index 0000000..b689398 --- /dev/null +++ b/apis/admin/payment.api @@ -0,0 +1,81 @@ +syntax = "v1" + +info ( + title: "payment API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +type ( + CreatePaymentMethodRequest { + Name string `json:"name" validate:"required"` + Platform string `json:"platform" validate:"required"` + Description string `json:"description"` + Icon string `json:"icon,omitempty"` + Domain string `json:"domain,omitempty"` + Config interface{} `json:"config" validate:"required"` + FeeMode uint `json:"fee_mode"` + FeePercent int64 `json:"fee_percent,omitempty"` + FeeAmount int64 `json:"fee_amount,omitempty"` + Enable *bool `json:"enable" validate:"required"` + } + UpdatePaymentMethodRequest { + Id int64 `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` + Platform string `json:"platform" validate:"required"` + Description string `json:"description"` + Icon string `json:"icon,omitempty"` + Domain string `json:"domain,omitempty"` + Config interface{} `json:"config" validate:"required"` + FeeMode uint `json:"fee_mode"` + FeePercent int64 `json:"fee_percent,omitempty"` + FeeAmount int64 `json:"fee_amount,omitempty"` + Enable *bool `json:"enable" validate:"required"` + } + DeletePaymentMethodRequest { + Id int64 `json:"id" validate:"required"` + } + GetPaymentMethodListRequest { + Page int `form:"page"` + Size int `form:"size"` + Platform string `form:"platform,omitempty"` + Search string `form:"search,omitempty"` + Enable *bool `form:"enable,omitempty"` + } + GetPaymentMethodListResponse { + Total int64 `json:"total"` + List []PaymentMethodDetail `json:"list"` + } +) + +@server ( + prefix: v1/admin/payment + group: admin/payment + middleware: AuthMiddleware +) +service ppanel { + @doc "Create Payment Method" + @handler CreatePaymentMethod + post / (CreatePaymentMethodRequest) returns (PaymentConfig) + + @doc "Update Payment Method" + @handler UpdatePaymentMethod + put / (UpdatePaymentMethodRequest) returns (PaymentConfig) + + @doc "Delete Payment Method" + @handler DeletePaymentMethod + delete / (DeletePaymentMethodRequest) + + @doc "Get Payment Method List" + @handler GetPaymentMethodList + get /list (GetPaymentMethodListRequest) returns (GetPaymentMethodListResponse) + + @doc "Get supported payment platform" + @handler GetPaymentPlatform + get /platform returns (PlatformResponse) +} + diff --git a/apis/admin/server.api b/apis/admin/server.api new file mode 100644 index 0000000..d6181b0 --- /dev/null +++ b/apis/admin/server.api @@ -0,0 +1,190 @@ +syntax = "v1" + +info ( + title: "Node API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +type ( + GetNodeServerListRequest { + Page int `form:"page" validate:"required"` + Size int `form:"size" validate:"required"` + Tag string `form:"tag,omitempty"` + GroupId int64 `form:"group_id,omitempty"` + Search string `form:"search,omitempty"` + } + GetNodeServerListResponse { + Total int64 `json:"total"` + List []Server `json:"list"` + } + UpdateNodeRequest { + Id int64 `json:"id" validate:"required"` + Tags []string `json:"tags"` + Country string `json:"country"` + City string `json:"city"` + Name string `json:"name" validate:"required"` + ServerAddr string `json:"server_addr" validate:"required"` + RelayMode string `json:"relay_mode"` + RelayNode []NodeRelay `json:"relay_node"` + SpeedLimit int `json:"speed_limit"` + TrafficRatio float32 `json:"traffic_ratio"` + GroupId int64 `json:"group_id"` + Protocol string `json:"protocol" validate:"required"` + Config interface{} `json:"config" validate:"required"` + Enable *bool `json:"enable"` + Sort int64 `json:"sort"` + } + CreateNodeRequest { + Name string `json:"name" validate:"required"` + Tags []string `json:"tags"` + Country string `json:"country"` + City string `json:"city"` + ServerAddr string `json:"server_addr" validate:"required"` + RelayMode string `json:"relay_mode"` + RelayNode []NodeRelay `json:"relay_node"` + SpeedLimit int `json:"speed_limit"` + TrafficRatio float32 `json:"traffic_ratio"` + GroupId int64 `json:"group_id"` + Protocol string `json:"protocol" validate:"required"` + Config interface{} `json:"config" validate:"required"` + Enable *bool `json:"enable"` + Sort int64 `json:"sort"` + } + DeleteNodeRequest { + Id int64 `json:"id" validate:"required"` + } + GetNodeGroupListResponse { + Total int64 `json:"total"` + List []ServerGroup `json:"list"` + } + CreateNodeGroupRequest { + Name string `json:"name" validate:"required"` + Description string `json:"description"` + } + UpdateNodeGroupRequest { + Id int64 `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` + Description string `json:"description"` + } + DeleteNodeGroupRequest { + Id int64 `json:"id" validate:"required"` + } + BatchDeleteNodeRequest { + Ids []int64 `json:"ids" validate:"required"` + } + BatchDeleteNodeGroupRequest { + Ids []int64 `json:"ids" validate:"required"` + } + GetNodeDetailRequest { + Id int64 `form:"id" validate:"required"` + } + NodeSortRequest { + Sort []SortItem `json:"sort"` + } + CreateRuleGroupRequest { + Name string `json:"name" validate:"required"` + Icon string `json:"icon"` + Tags []string `json:"tags"` + Rules string `json:"rules"` + Enable bool `json:"enable"` + } + UpdateRuleGroupRequest { + Id int64 `json:"id" validate:"required"` + Icon string `json:"icon"` + Name string `json:"name" validate:"required"` + Tags []string `json:"tags"` + Rules string `json:"rules"` + Enable bool `json:"enable"` + } + DeleteRuleGroupRequest { + Id int64 `json:"id" validate:"required"` + } + GetRuleGroupResponse { + Total int64 `json:"total"` + List []ServerRuleGroup `json:"list"` + } + GetNodeTagListResponse { + Tags []string `json:"tags"` + } +) + +@server ( + prefix: v1/admin/server + group: admin/server + middleware: AuthMiddleware +) +service ppanel { + @doc "Get node tag list" + @handler GetNodeTagList + get /tag/list returns (GetNodeTagListResponse) + + @doc "Get node list" + @handler GetNodeList + get /list (GetNodeServerListRequest) returns (GetNodeServerListResponse) + + @doc "Get node detail" + @handler GetNodeDetail + get /detail (GetNodeDetailRequest) returns (Server) + + @doc "Update node" + @handler UpdateNode + put / (UpdateNodeRequest) + + @doc "Create node" + @handler CreateNode + post / (CreateNodeRequest) + + @doc "Delete node" + @handler DeleteNode + delete / (DeleteNodeRequest) + + @doc "Batch delete node" + @handler BatchDeleteNode + delete /batch (BatchDeleteNodeRequest) + + @doc "Get node group list" + @handler GetNodeGroupList + get /group/list returns (GetNodeGroupListResponse) + + @doc "Create node group" + @handler CreateNodeGroup + post /group (CreateNodeGroupRequest) + + @doc "Update node group" + @handler UpdateNodeGroup + put /group (UpdateNodeGroupRequest) + + @doc "Delete node group" + @handler DeleteNodeGroup + delete /group (DeleteNodeGroupRequest) + + @doc "Batch delete node group" + @handler BatchDeleteNodeGroup + delete /group/batch (BatchDeleteNodeGroupRequest) + + @doc "Node sort " + @handler NodeSort + post /sort (NodeSortRequest) + + @doc "Create rule group" + @handler CreateRuleGroup + post /rule_group (CreateRuleGroupRequest) + + @doc "Update rule group" + @handler UpdateRuleGroup + put /rule_group (UpdateRuleGroupRequest) + + @doc "Delete rule group" + @handler DeleteRuleGroup + delete /rule_group (DeleteRuleGroupRequest) + + @doc "Get rule group list" + @handler GetRuleGroupList + get /rule_group_list returns (GetRuleGroupResponse) +} + diff --git a/apis/admin/subscribe.api b/apis/admin/subscribe.api new file mode 100644 index 0000000..d95ab39 --- /dev/null +++ b/apis/admin/subscribe.api @@ -0,0 +1,164 @@ +syntax = "v1" + +info ( + title: "Subscribe API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +type ( + GetSubscribeDetailsRequest { + Id int64 `form:"id" validate:"required"` + } + CreateSubscribeGroupRequest { + Name string `json:"name" validate:"required"` + Description string `json:"description"` + } + UpdateSubscribeGroupRequest { + Id int64 `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` + Description string `json:"description"` + } + GetSubscribeGroupListResponse { + List []SubscribeGroup `json:"list"` + Total int64 `json:"total"` + } + DeleteSubscribeGroupRequest { + Id int64 `json:"id" validate:"required"` + } + BatchDeleteSubscribeGroupRequest { + Ids []int64 `json:"ids" validate:"required"` + } + CreateSubscribeRequest { + Name string `json:"name" validate:"required"` + Description string `json:"description"` + UnitPrice int64 `json:"unit_price"` + UnitTime string `json:"unit_time"` + Discount []SubscribeDiscount `json:"discount"` + Replacement int64 `json:"replacement"` + Inventory int64 `json:"inventory"` + Traffic int64 `json:"traffic"` + SpeedLimit int64 `json:"speed_limit"` + DeviceLimit int64 `json:"device_limit"` + Quota int64 `json:"quota"` + GroupId int64 `json:"group_id"` + ServerGroup []int64 `json:"server_group"` + Server []int64 `json:"server"` + Show *bool `json:"show"` + Sell *bool `json:"sell"` + DeductionRatio int64 `json:"deduction_ratio"` + AllowDeduction *bool `json:"allow_deduction"` + ResetCycle int64 `json:"reset_cycle"` + RenewalReset *bool `json:"renewal_reset"` + } + UpdateSubscribeRequest { + Id int64 `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` + Description string `json:"description"` + UnitPrice int64 `json:"unit_price"` + UnitTime string `json:"unit_time"` + Discount []SubscribeDiscount `json:"discount"` + Replacement int64 `json:"replacement"` + Inventory int64 `json:"inventory"` + Traffic int64 `json:"traffic"` + SpeedLimit int64 `json:"speed_limit"` + DeviceLimit int64 `json:"device_limit"` + Quota int64 `json:"quota"` + GroupId int64 `json:"group_id"` + ServerGroup []int64 `json:"server_group"` + Server []int64 `json:"server"` + Show *bool `json:"show"` + Sell *bool `json:"sell"` + Sort int64 `json:"sort"` + DeductionRatio int64 `json:"deduction_ratio"` + AllowDeduction *bool `json:"allow_deduction"` + ResetCycle int64 `json:"reset_cycle"` + RenewalReset *bool `json:"renewal_reset"` + } + SubscribeSortRequest { + Sort []SortItem `json:"sort"` + } + GetSubscribeListRequest { + Page int64 `form:"page" validate:"required"` + Size int64 `form:"size" validate:"required"` + GroupId int64 `form:"group_id,omitempty"` + Search string `form:"search,omitempty"` + } + + SubscribeItem { + Subscribe + + Sold int64 `json:"sold"` + } + + GetSubscribeListResponse { + List []SubscribeItem `json:"list"` + Total int64 `json:"total"` + } + DeleteSubscribeRequest { + Id int64 `json:"id" validate:"required"` + } + BatchDeleteSubscribeRequest { + Ids []int64 `json:"ids" validate:"required"` + } +) + +@server ( + prefix: v1/admin/subscribe + group: admin/subscribe + middleware: AuthMiddleware +) +service ppanel { + @doc "Create subscribe group" + @handler CreateSubscribeGroup + post /group (CreateSubscribeGroupRequest) + + @doc "Update subscribe group" + @handler UpdateSubscribeGroup + put /group (UpdateSubscribeGroupRequest) + + @doc "Get subscribe group list" + @handler GetSubscribeGroupList + get /group/list returns (GetSubscribeGroupListResponse) + + @doc "Delete subscribe group" + @handler DeleteSubscribeGroup + delete /group (DeleteSubscribeGroupRequest) + + @doc "Batch delete subscribe group" + @handler BatchDeleteSubscribeGroup + delete /group/batch (BatchDeleteSubscribeGroupRequest) + + @doc "Create subscribe" + @handler CreateSubscribe + post / (CreateSubscribeRequest) + + @doc "Update subscribe" + @handler UpdateSubscribe + put / (UpdateSubscribeRequest) + + @doc "Get subscribe list" + @handler GetSubscribeList + get /list (GetSubscribeListRequest) returns (GetSubscribeListResponse) + + @doc "Delete subscribe" + @handler DeleteSubscribe + delete / (DeleteSubscribeRequest) + + @doc "Batch delete subscribe" + @handler BatchDeleteSubscribe + delete /batch (BatchDeleteSubscribeRequest) + + @doc "Get subscribe details" + @handler GetSubscribeDetails + get /details (GetSubscribeDetailsRequest) returns (Subscribe) + + @doc "Subscribe sort" + @handler SubscribeSort + post /sort (SubscribeSortRequest) +} + diff --git a/apis/admin/system.api b/apis/admin/system.api new file mode 100644 index 0000000..09323c3 --- /dev/null +++ b/apis/admin/system.api @@ -0,0 +1,205 @@ +syntax = "v1" + +info ( + title: "System API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +type ( + // Update application request + UpdateApplicationRequest { + Id int64 `json:"id" validate:"required"` + Icon string `json:"icon"` + Name string `json:"name"` + Description string `json:"description"` + SubscribeType string `json:"subscribe_type"` + Platform ApplicationPlatform `json:"platform"` + } + // Create application request + CreateApplicationRequest { + Icon string `json:"icon"` + Name string `json:"name"` + Description string `json:"description"` + SubscribeType string `json:"subscribe_type"` + Platform ApplicationPlatform `json:"platform"` + } + // Update application request + UpdateApplicationVersionRequest { + Id int64 `json:"id" validate:"required"` + Url string `json:"url"` + Version string `json:"version" validate:"required"` + Description string `json:"description"` + Platform string `json:"platform" validate:"required,oneof=windows mac linux android ios harmony"` + IsDefault bool `json:"is_default"` + ApplicationId int64 `json:"application_id" validate:"required"` + } + // Create application request + CreateApplicationVersionRequest { + Url string `json:"url"` + Version string `json:"version" validate:"required"` + Description string `json:"description"` + Platform string `json:"platform" validate:"required,oneof=windows mac linux android ios harmony"` + IsDefault bool `json:"is_default"` + ApplicationId int64 `json:"application_id" validate:"required"` + } + // Delete application request + DeleteApplicationRequest { + Id int64 `json:"id" validate:"required"` + } + // Delete application request + DeleteApplicationVersionRequest { + Id int64 `json:"id" validate:"required"` + } + GetNodeMultiplierResponse { + Periods []TimePeriod `json:"periods"` + } + // SetNodeMultiplierRequest + SetNodeMultiplierRequest { + Periods []TimePeriod `json:"periods"` + } +) + +@server ( + prefix: v1/admin/system + group: admin/system + middleware: AuthMiddleware +) +service ppanel { + @doc "Get site config" + @handler GetSiteConfig + get /site_config returns (SiteConfig) + + @doc "Update site config" + @handler UpdateSiteConfig + put /site_config (SiteConfig) + + @doc "Get subscribe config" + @handler GetSubscribeConfig + get /subscribe_config returns (SubscribeConfig) + + @doc "Update subscribe config" + @handler UpdateSubscribeConfig + put /subscribe_config (SubscribeConfig) + + @doc "Get subscribe type" + @handler GetSubscribeType + get /subscribe_type returns (SubscribeType) + + @doc "update application config" + @handler UpdateApplicationConfig + put /application_config (ApplicationConfig) + + @doc "get application config" + @handler GetApplicationConfig + get /application_config returns (ApplicationConfig) + + @doc "Get application" + @handler GetApplication + get /application returns (ApplicationResponse) + + @doc "Update application" + @handler UpdateApplication + put /application (UpdateApplicationRequest) + + @doc "Create application" + @handler CreateApplication + post /application (CreateApplicationRequest) + + @doc "Delete application" + @handler DeleteApplication + delete /application (DeleteApplicationRequest) + + @doc "Update application version" + @handler UpdateApplicationVersion + put /application_version (UpdateApplicationVersionRequest) + + @doc "Create application version" + @handler CreateApplicationVersion + post /application_version (CreateApplicationVersionRequest) + + @doc "Delete application" + @handler DeleteApplicationVersion + delete /application_version (DeleteApplicationVersionRequest) + + @doc "Get register config" + @handler GetRegisterConfig + get /register_config returns (RegisterConfig) + + @doc "Update register config" + @handler UpdateRegisterConfig + put /register_config (RegisterConfig) + + @doc "Get verify config" + @handler GetVerifyConfig + get /verify_config returns (VerifyConfig) + + @doc "Update verify config" + @handler UpdateVerifyConfig + put /verify_config (VerifyConfig) + + @doc "Get node config" + @handler GetNodeConfig + get /node_config returns (NodeConfig) + + @doc "Update node config" + @handler UpdateNodeConfig + put /node_config (NodeConfig) + + @doc "Get invite config" + @handler GetInviteConfig + get /invite_config returns (InviteConfig) + + @doc "Update invite config" + @handler UpdateInviteConfig + put /invite_config (InviteConfig) + + @doc "Get Team of Service Config" + @handler GetTosConfig + get /tos_config returns (TosConfig) + + @doc "Update Team of Service Config" + @handler UpdateTosConfig + put /tos_config (TosConfig) + + @doc "get Privacy Policy Config" + @handler GetPrivacyPolicyConfig + get /privacy returns (PrivacyPolicyConfig) + + @doc "Update Privacy Policy Config" + @handler UpdatePrivacyPolicyConfig + put /privacy (PrivacyPolicyConfig) + + @doc "Get Currency Config" + @handler GetCurrencyConfig + get /currency_config returns (CurrencyConfig) + + @doc "Update Currency Config" + @handler UpdateCurrencyConfig + put /currency_config (CurrencyConfig) + + @doc "setting telegram bot" + @handler SettingTelegramBot + post /setting_telegram_bot + + @doc "Get Node Multiplier" + @handler GetNodeMultiplier + get /get_node_multiplier returns (GetNodeMultiplierResponse) + + @doc "Set Node Multiplier" + @handler SetNodeMultiplier + post /set_node_multiplier (SetNodeMultiplierRequest) + + @doc "Get Verify Code Config" + @handler GetVerifyCodeConfig + get /verify_code_config returns (VerifyCodeConfig) + + @doc "Update Verify Code Config" + @handler UpdateVerifyCodeConfig + put /verify_code_config (VerifyCodeConfig) +} + diff --git a/apis/admin/ticket.api b/apis/admin/ticket.api new file mode 100644 index 0000000..59ae897 --- /dev/null +++ b/apis/admin/ticket.api @@ -0,0 +1,62 @@ +syntax = "v1" + +info ( + title: "Ticket API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +type ( + UpdateTicketStatusRequest { + Id int64 `json:"id" validate:"required"` + Status *uint8 `json:"status" validate:"required"` + } + GetTicketListRequest { + Page int64 `form:"page"` + Size int64 `form:"size"` + UserId int64 `form:"user_id,omitempty"` + Status *uint8 `form:"status,omitempty"` + Search string `form:"search,omitempty"` + } + GetTicketListResponse { + Total int64 `json:"total"` + List []Ticket `json:"list"` + } + GetTicketRequest { + Id int64 `form:"id" validate:"required"` + } + CreateTicketFollowRequest { + TicketId int64 `json:"ticket_id" validate:"required"` + From string `json:"from" validate:"required"` + Type uint8 `json:"type" validate:"required"` + Content string `json:"content" validate:"required"` + } +) + +@server ( + prefix: v1/admin/ticket + group: admin/ticket + middleware: AuthMiddleware +) +service ppanel { + @doc "Get ticket list" + @handler GetTicketList + get /list (GetTicketListRequest) returns (GetTicketListResponse) + + @doc "Get ticket detail" + @handler GetTicket + get /detail (GetTicketRequest) returns (Ticket) + + @doc "Update ticket status" + @handler UpdateTicketStatus + put / (UpdateTicketStatusRequest) + + @doc "Create ticket follow" + @handler CreateTicketFollow + post /follow (CreateTicketFollowRequest) +} + diff --git a/apis/admin/tool.api b/apis/admin/tool.api new file mode 100644 index 0000000..f54ee61 --- /dev/null +++ b/apis/admin/tool.api @@ -0,0 +1,33 @@ +syntax = "v1" + +info( + title: "Tools Api" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +type ( + LogResponse { + List interface{} `json:"list"` + } +) + +@server ( + prefix: v1/admin/tool + group: admin/tool + middleware: AuthMiddleware +) + +service ppanel { + @doc "Get System Log" + @handler GetSystemLog + get /log returns (LogResponse) + + @doc "Restart System" + @handler RestartSystem + get /restart +} diff --git a/apis/admin/user.api b/apis/admin/user.api new file mode 100644 index 0000000..92149fd --- /dev/null +++ b/apis/admin/user.api @@ -0,0 +1,278 @@ +syntax = "v1" + +info ( + title: "User API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import ( + "../types.api" +) + +type ( + // GetUserListRequest + GetUserListRequest { + Page int `form:"page"` + Size int `form:"size"` + Search string `form:"search,omitempty"` + UserId *int64 `form:"user_id,omitempty"` + SubscribeId *int64 `form:"subscribe_id,omitempty"` + UserSubscribeId *int64 `form:"user_subscribe_id,omitempty"` + } + // GetUserListResponse + GetUserListResponse { + Total int64 `json:"total"` + List []User `json:"list"` + } + // GetUserDetail + GetDetailRequest { + Id int64 `form:"id" validate:"required"` + } + UpdateUserBasiceInfoRequest { + UserId int64 `json:"user_id" validate:"required"` + Password string `json:"password"` + Avatar string `json:"avatar"` + Balance int64 `json:"balance"` + Commission int64 `json:"commission"` + GiftAmount int64 `json:"gift_amount"` + Telegram int64 `json:"telegram"` + ReferCode string `json:"refer_code"` + RefererId int64 `json:"referer_id"` + Enable bool `json:"enable"` + IsAdmin bool `json:"is_admin"` + } + UpdateUserNotifySettingRequest { + UserId int64 `json:"user_id" validate:"required"` + EnableBalanceNotify bool `json:"enable_balance_notify"` + EnableLoginNotify bool `json:"enable_login_notify"` + EnableSubscribeNotify bool `json:"enable_subscribe_notify"` + EnableTradeNotify bool `json:"enable_trade_notify"` + } + CreateUserRequest { + Email string `json:"email"` + Telephone string `json:"telephone"` + TelephoneAreaCode string `json:"telephone_area_code"` + Password string `json:"password"` + ProductId int64 `json:"product_id"` + Duration int64 `json:"duration"` + RefererUser string `json:"referer_user"` + ReferCode string `json:"refer_code"` + Balance int64 `json:"balance"` + Commission int64 `json:"commission"` + GiftAmount int64 `json:"gift_amount"` + IsAdmin bool `json:"is_admin"` + } + UserSubscribeDetail { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + User User `json:"user"` + OrderId int64 `json:"order_id"` + SubscribeId int64 `json:"subscribe_id"` + Subscribe Subscribe `json:"subscribe"` + StartTime int64 `json:"start_time"` + ExpireTime int64 `json:"expire_time"` + ResetTime int64 `json:"reset_time"` + Traffic int64 `json:"traffic"` + Download int64 `json:"download"` + Upload int64 `json:"upload"` + Token string `json:"token"` + Status uint8 `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + BatchDeleteUserRequest { + Ids []int64 `json:"ids" validate:"required"` + } + DeleteUserDeivceRequest { + Id int64 `json:"id"` + } + KickOfflineRequest { + Id int64 `json:"id"` + } + CreateUserAuthMethodRequest { + UserId int64 `json:"user_id"` + AuthType string `json:"auth_type"` + AuthIdentifier string `json:"auth_identifier"` + } + DeleteUserAuthMethodRequest { + UserId int64 `json:"user_id"` + AuthType string `json:"auth_type"` + } + UpdateUserAuthMethodRequest { + UserId int64 `json:"user_id"` + AuthType string `json:"auth_type"` + AuthIdentifier string `json:"auth_identifier"` + } + GetUserAuthMethodRequest { + UserId int64 `json:"user_id"` + } + GetUserAuthMethodResponse { + AuthMethods []UserAuthMethod `json:"auth_methods"` + } + GetUserSubscribeListRequest { + Page int `form:"page"` + Size int `form:"size"` + UserId int64 `form:"user_id"` + } + GetUserSubscribeListResponse { + List []UserSubscribe `json:"list"` + Total int64 `json:"total"` + } + GetUserSubscribeLogsRequest { + Page int `form:"page"` + Size int `form:"size"` + UserId int64 `form:"user_id"` + SubscribeId int64 `form:"subscribe_id,omitempty"` + } + GetUserSubscribeLogsResponse { + List []UserSubscribeLog `json:"list"` + Total int64 `json:"total"` + } + GetUserSubscribeDevicesRequest { + Page int `form:"page"` + Size int `form:"size"` + UserId int64 `form:"user_id"` + SubscribeId int64 `form:"subscribe_id"` + } + GetUserSubscribeDevicesResponse { + List []UserDevice `json:"list"` + Total int64 `json:"total"` + } + CreateUserSubscribeRequest { + UserId int64 `json:"user_id"` + ExpiredAt int64 `json:"expired_at"` + Traffic int64 `json:"traffic"` + SubscribeId int64 `json:"subscribe_id"` + } + UpdateUserSubscribeRequest { + UserSubscribeId int64 `json:"user_subscribe_id"` + SubscribeId int64 `json:"subscribe_id"` + Traffic int64 `json:"traffic"` + ExpiredAt int64 `json:"expired_at"` + Upload int64 `json:"upload"` + Download int64 `json:"download"` + } + GetUserLoginLogsRequest { + Page int `form:"page"` + Size int `form:"size"` + UserId int64 `form:"user_id"` + } + GetUserLoginLogsResponse { + List []UserLoginLog `json:"list"` + Total int64 `json:"total"` + } + DeleteUserSubscribeRequest { + UserSubscribeId int64 `json:"user_subscribe_id"` + } + GetUserSubscribeByIdRequest { + Id int64 `form:"id" validate:"required"` + } +) + +@server ( + prefix: v1/admin/user + group: admin/user + jwt: JwtAuth + middleware: AuthMiddleware +) +service ppanel { + @doc "Get user list" + @handler GetUserList + get /list (GetUserListRequest) returns (GetUserListResponse) + + @doc "Get user detail" + @handler GetUserDetail + get /detail (GetDetailRequest) returns (User) + + @doc "Update user basic info" + @handler UpdateUserBasicInfo + put /basic (UpdateUserBasiceInfoRequest) + + @doc "Update user notify setting" + @handler UpdateUserNotifySetting + put /notify (UpdateUserNotifySettingRequest) + + @doc "Delete user" + @handler DeleteUser + delete / (GetDetailRequest) + + @doc "Current user" + @handler CurrentUser + get /current returns (User) + + @doc "Batch delete user" + @handler BatchDeleteUser + delete /batch (BatchDeleteUserRequest) + + @doc "Create user" + @handler CreateUser + post / (CreateUserRequest) + + @doc "User device" + @handler UpdateUserDevice + put /device (UserDevice) + + @doc "Delete user device" + @handler DeleteUserDevice + delete /device (DeleteUserDeivceRequest) + + @doc "kick offline user device" + @handler KickOfflineByUserDevice + put /device/kick_offline (KickOfflineRequest) + + @doc "Create user auth method" + @handler CreateUserAuthMethod + post /auth_method (CreateUserAuthMethodRequest) + + @doc "Delete user auth method" + @handler DeleteUserAuthMethod + delete /auth_method (DeleteUserAuthMethodRequest) + + @doc "Update user auth method" + @handler UpdateUserAuthMethod + put /auth_method (UpdateUserAuthMethodRequest) + + @doc "Get user auth method" + @handler GetUserAuthMethod + get /auth_method (GetUserAuthMethodRequest) returns (GetUserAuthMethodResponse) + + @doc "Get user subcribe" + @handler GetUserSubscribe + get /subscribe (GetUserSubscribeListRequest) returns (GetUserSubscribeListResponse) + + @doc "Get user subcribe by id" + @handler GetUserSubscribeById + get /subscribe/detail (GetUserSubscribeByIdRequest) returns (UserSubscribeDetail) + + @doc "Get user subcribe logs" + @handler GetUserSubscribeLogs + get /subscribe/logs (GetUserSubscribeLogsRequest) returns (GetUserSubscribeLogsResponse) + + @doc "Get user subcribe traffic logs" + @handler GetUserSubscribeTrafficLogs + get /subscribe/traffic_logs (GetUserSubscribeTrafficLogsRequest) returns (GetUserSubscribeTrafficLogsResponse) + + @doc "Get user subcribe devices" + @handler GetUserSubscribeDevices + get /subscribe/device (GetUserSubscribeDevicesRequest) returns (GetUserSubscribeDevicesResponse) + + @doc "Create user subcribe" + @handler CreateUserSubscribe + post /subscribe (CreateUserSubscribeRequest) + + @doc "Update user subcribe" + @handler UpdateUserSubscribe + put /subscribe (UpdateUserSubscribeRequest) + + @doc "Delete user subcribe" + @handler DeleteUserSubscribe + delete /subscribe (DeleteUserSubscribeRequest) + + @doc "Get user login logs" + @handler GetUserLoginLogs + get /login/logs (GetUserLoginLogsRequest) returns (GetUserLoginLogsResponse) +} + diff --git a/apis/app/announcement.api b/apis/app/announcement.api new file mode 100644 index 0000000..2decd09 --- /dev/null +++ b/apis/app/announcement.api @@ -0,0 +1,24 @@ +syntax = "v1" + +info ( + title: "Announcement API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + + +@server ( + prefix: v1/app/announcement + group: app/announcement + middleware: AppMiddleware,AuthMiddleware +) +service ppanel { + @doc "Query announcement" + @handler QueryAnnouncement + get /list (QueryAnnouncementRequest) returns (QueryAnnouncementResponse) +} + diff --git a/apis/app/auth.api b/apis/app/auth.api new file mode 100644 index 0000000..015eb64 --- /dev/null +++ b/apis/app/auth.api @@ -0,0 +1,105 @@ +syntax = "v1" + +info( + title: "App Auth Api" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import ( + "../types.api" +) + +type ( + AppAuthCheckRequest { + Method string `json:"method" validate:"required" validate:"required,oneof=device email mobile"` + Account string `json:"account"` + Identifier string `json:"identifier" validate:"required"` + UserAgent string `json:"user_agent" validate:"required,oneof=windows mac linux android ios harmony"` + AreaCode string `json:"area_code"` + } + AppAuthCheckResponse { + Status bool + } + AppAuthRequest { + Method string `json:"method" validate:"required" validate:"required,oneof=device email mobile"` + Account string `json:"account"` + Password string `json:"password"` + Identifier string `json:"identifier" validate:"required"` + UserAgent string `json:"user_agent" validate:"required,oneof=windows mac linux android ios harmony"` + Code string `json:"code"` + Invite string `json:"invite"` + AreaCode string `json:"area_code"` + CfToken string `json:"cf_token,optional"` + } + AppAuthRespone { + Token string `json:"token"` + } + AppSendCodeRequest { + Method string `json:"method" validate:"required" validate:"required,oneof=email mobile"` + Account string `json:"account"` + AreaCode string `json:"area_code"` + CfToken string `json:"cf_token,optional"` + } + AppSendCodeRespone { + Status bool `json:"status"` + Code string `json:"code,omitempty"` + } + AppConfigRequest { + UserAgent string `json:"user_agent" validate:"required,oneof=windows mac linux android ios harmony"` + } + AppConfigResponse { + EncryptionKey string `json:"encryption_key"` + EncryptionMethod string `json:"encryption_method"` + Domains []string `json:"domains"` + StartupPicture string `json:"startup_picture"` + StartupPictureSkipTime int64 `json:"startup_picture_skip_time"` + Application AppInfo `json:"applications"` + OfficialEmail string `json:"official_email"` + OfficialWebsite string `json:"official_website"` + OfficialTelegram string `json:"official_telegram"` + OfficialTelephone string `json:"official_telephone"` + InvitationLink string `json:"invitation_link"` + KrWebsiteId string `json:"kr_website_id"` + } + AppInfo { + Id int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Url string `json:"url"` + Version string `json:"version"` + VersionReview string `json:"version_review"` + VersionDescription string `json:"version_description"` + IsDefault bool `json:"is_default"` + } +) + +@server( + prefix: v1/app/auth + group: app/auth + middleware: AppMiddleware +) +service ppanel { + @doc "Check Account" + @handler Check + post /check (AppAuthCheckRequest) returns (AppAuthCheckResponse) + + @doc "Login" + @handler Login + post /login (AppAuthRequest) returns (AppAuthRespone) + + @doc "Register" + @handler Register + post /register (AppAuthRequest) returns (AppAuthRespone) + + @doc "Reset Password" + @handler ResetPassword + post /reset_password (AppAuthRequest) returns (AppAuthRespone) + + @doc "GetAppConfig" + @handler GetAppConfig + post /config (AppConfigRequest) returns (AppConfigResponse) +} + diff --git a/apis/app/document.api b/apis/app/document.api new file mode 100644 index 0000000..6cc4c71 --- /dev/null +++ b/apis/app/document.api @@ -0,0 +1,27 @@ +syntax = "v1" + +info( + title: "Document API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +@server ( + prefix: v1/app/document + group: app/document + middleware: AppMiddleware,AuthMiddleware +) + +service ppanel { + @doc "Get document list" + @handler QueryDocumentList + get /list returns (QueryDocumentListResponse) + + @doc "Get document detail" + @handler QueryDocumentDetail + get /detail (QueryDocumentDetailRequest) returns (Document) +} \ No newline at end of file diff --git a/apis/app/node.api b/apis/app/node.api new file mode 100644 index 0000000..b2434b2 --- /dev/null +++ b/apis/app/node.api @@ -0,0 +1,49 @@ +syntax = "v1" + + +info( + title: "App Node Api" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +type( + + + AppRuleGroupListResponse { + Total int64 `json:"total"` + List []ServerRuleGroup `json:"list"` + } + + AppUserSubscbribeNodeRequest { + Id int64 `form:"id" validate:"required"` + } + + AppUserSubscbribeNodeResponse{ + List []AppUserSubscbribeNode `json:"list"` + } +) + +@server ( + prefix: v1/app/node + group: app/node + middleware: AppMiddleware,AuthMiddleware +) + +service ppanel { + + + + @doc "Get Node list" + @handler GetNodeList + get /list (AppUserSubscbribeNodeRequest) returns(AppUserSubscbribeNodeResponse) + + @doc "Get rule group list" + @handler GetRuleGroupList + get /rule_group_list returns (AppRuleGroupListResponse) + +} \ No newline at end of file diff --git a/apis/app/order.api b/apis/app/order.api new file mode 100644 index 0000000..959ff40 --- /dev/null +++ b/apis/app/order.api @@ -0,0 +1,58 @@ +syntax = "v1" + +info ( + title: "Order API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import ( + "../types.api" +) + + +@server ( + prefix: v1/app/order + group: app/order + middleware: AppMiddleware,AuthMiddleware +) +service ppanel { + @doc "Pre create order" + @handler PreCreateOrder + post /pre (PurchaseOrderRequest) returns (PreOrderResponse) + + @doc "purchase Subscription" + @handler Purchase + post /purchase (PurchaseOrderRequest) returns (PurchaseOrderResponse) + + @doc "Renewal Subscription" + @handler Renewal + post /renewal (RenewalOrderRequest) returns (RenewalOrderResponse) + + @doc "Reset traffic" + @handler ResetTraffic + post /reset (ResetTrafficOrderRequest) returns (ResetTrafficOrderResponse) + + @doc "Recharge" + @handler Recharge + post /recharge (RechargeOrderRequest) returns (RechargeOrderResponse) + + @doc "Checkout order" + @handler CheckoutOrder + post /checkout (CheckoutOrderRequest) returns (CheckoutOrderResponse) + + @doc "Close order" + @handler CloseOrder + post /close (CloseOrderRequest) + + @doc "Get order" + @handler QueryOrderDetail + get /detail (QueryOrderDetailRequest) returns (OrderDetail) + + @doc "Get order list" + @handler QueryOrderList + get /list (QueryOrderListRequest) returns (QueryOrderListResponse) +} + diff --git a/apis/app/payment.api b/apis/app/payment.api new file mode 100644 index 0000000..9769a47 --- /dev/null +++ b/apis/app/payment.api @@ -0,0 +1,23 @@ +syntax = "v1" + +info ( + title: "payment API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +@server ( + prefix: v1/app/payment + group: app/payment + middleware: AppMiddleware,AuthMiddleware +) +service ppanel { + @doc "Get available payment methods" + @handler GetAvailablePaymentMethods + get /methods returns (GetAvailablePaymentMethodsResponse) +} + diff --git a/apis/app/subscribe.api b/apis/app/subscribe.api new file mode 100644 index 0000000..0a4a05a --- /dev/null +++ b/apis/app/subscribe.api @@ -0,0 +1,75 @@ +syntax = "v1" + +info( + title: "Subscribe API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + + +type ( + QueryUserSubscribeResp { + Data []UserSubscribeData `json:"data"` + } + + UserSubscribeData { + SubscribeId int64 `json:"subscribe_id"` + UserSubscribeId int64 `json:"user_subscribe_id"` + } + + + UserSubscribeResetPeriodRequest { + UserSubscribeId int64 `json:"user_subscribe_id"` + } + + UserSubscribeResetPeriodResponse { + Status bool `json:"status"` + } + + AppUserSubscribeRequest { + ContainsNodes *bool `form:"contains_nodes"` + } + + AppUserSubscbribeResponse { + List []AppUserSubcbribe `json:"list"` + } +) + +@server( + prefix: v1/app/subscribe + group: app/subscribe + middleware: AppMiddleware,AuthMiddleware +) + + +service ppanel { + @doc "Get subscribe list" + @handler QuerySubscribeList + get /list returns (QuerySubscribeListResponse) + + @doc "Get subscribe group list" + @handler QuerySubscribeGroupList + get /group/list returns (QuerySubscribeGroupListResponse) + + @doc "Get application config" + @handler QueryApplicationConfig + get /application/config returns (ApplicationResponse) + + @doc "Get Already subscribed to package" + @handler QueryUserAlreadySubscribe + get /user/already_subscribe returns (QueryUserSubscribeResp) + + + @doc "Get Available subscriptions for users" + @handler QueryUserAvailableUserSubscribe + get /user/available_subscribe (AppUserSubscribeRequest) returns (AppUserSubscbribeResponse) + + @doc "Reset user subscription period" + @handler ResetUserSubscribePeriod + post /reset/period (UserSubscribeResetPeriodRequest) returns (UserSubscribeResetPeriodResponse) +} + diff --git a/apis/app/user.api b/apis/app/user.api new file mode 100644 index 0000000..67fb380 --- /dev/null +++ b/apis/app/user.api @@ -0,0 +1,90 @@ +syntax = "v1" + +info ( + title: "App User Api" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import ( + "../types.api" +) + +type ( + UserInfoResponse { + Id int64 `json:"id"` + Balance int64 `json:"balance"` + Email string `json:"email"` + RefererId int64 `json:"referer_id"` + ReferCode string `json:"refer_code"` + Avatar string `json:"avatar"` + AreaCode string `json:"area_code"` + Telephone string `json:"telephone"` + Devices []UserDevice `json:"devices"` + AuthMethods []UserAuthMethod `json:"auth_methods"` + } + UpdatePasswordRequeset { + Password string `json:"password"` + NewPassword string `json:"new_password"` + } + DeleteAccountRequest { + Method string `json:"method" validate:"required" validate:"required,oneof=email telephone device"` + Code string `json:"code"` + } + + GetUserOnlineTimeStatisticsResponse{ + WeeklyStats []WeeklyStat`json:"weekly_stats"` + ConnectionRecords ConnectionRecords`json:"connection_records"` + } + + WeeklyStat{ + Day int `json:"day"` + DayName string `json:"day_name"` + Hours float64 `json:"hours"` + } + ConnectionRecords{ + CurrentContinuousDays int64 `json:"current_continuous_days"` + HistoryContinuousDays int64 `json:"history_continuous_days"` + LongestSingleConnection int64 `json:"longest_single_connection"` + } +) + +@server ( + prefix: v1/app/user + group: app/user + middleware: AppMiddleware,AuthMiddleware +) +service ppanel { + @doc "query user info" + @handler QueryUserInfo + get /info returns (UserInfoResponse) + + @doc "Update Password " + @handler UpdatePassword + put /password (UpdatePasswordRequeset) + + @doc "Delete Account" + @handler DeleteAccount + delete /account (DeleteAccountRequest) + + @doc "Get user subcribe traffic logs" + @handler GetUserSubscribeTrafficLogs + get /subscribe/traffic_logs (GetUserSubscribeTrafficLogsRequest) returns (GetUserSubscribeTrafficLogsResponse) + + @doc "Get user online time total" + @handler GetUserOnlineTimeStatistics + get /online_time/statistics returns (GetUserOnlineTimeStatisticsResponse) + + @doc "Query User Affiliate List" + @handler QueryUserAffiliateList + get /affiliate/list (QueryUserAffiliateListRequest) returns (QueryUserAffiliateListResponse) + + @doc "Query User Affiliate Count" + @handler QueryUserAffiliate + get /affiliate/count returns (QueryUserAffiliateCountResponse) + + +} + diff --git a/apis/app/ws.api b/apis/app/ws.api new file mode 100644 index 0000000..a3a26be --- /dev/null +++ b/apis/app/ws.api @@ -0,0 +1,23 @@ +syntax = "v1" + +info( + title: "App Heartbeat Api" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + + +@server( + prefix: v1/app/ws + group: app/ws + middleware: AuthMiddleware +) + + +service ppanel { + @doc "App heartbeat" + @handler AppWs + get /:userid/:identifier +} \ No newline at end of file diff --git a/apis/auth/auth.api b/apis/auth/auth.api new file mode 100644 index 0000000..22dbe10 --- /dev/null +++ b/apis/auth/auth.api @@ -0,0 +1,166 @@ +syntax = "v1" + +info ( + title: "User auth API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +type ( + // User login request + UserLoginRequest { + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + CfToken string `json:"cf_token,optional"` + } + // Check user is exist request + CheckUserRequest { + Email string `form:"email" validate:"required"` + } + // User login response + CheckUserResponse { + exist bool `json:"exist"` + } + // User login response + UserRegisterRequest { + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + Invite string `json:"invite,optional"` + Code string `json:"code,optional"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + CfToken string `json:"cf_token,optional"` + } + // User login response + ResetPasswordRequest { + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + Code string `json:"code,optional"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + CfToken string `json:"cf_token,optional"` + } + LoginResponse { + Token string `json:"token"` + } + OAthLoginRequest { + Method string `json:"method" validate:"required"` // google, facebook, apple, telegram, github etc. + Redirect string `json:"redirect"` + } + OAuthLoginResponse { + Redirect string `json:"redirect"` + } + + OAuthLoginGetTokenRequest { + Method string `json:"method" validate:"required"` // google, facebook, apple, telegram, github etc. + Callback interface{} `json:"callback" validate:"required"` + } + + // login request + TelephoneLoginRequest { + Telephone string `json:"telephone" validate:"required"` + TelephoneCode string `json:"telephone_code"` + TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` + Password string `json:"password"` + IP string `header:"X-Original-Forwarded-For"` + CfToken string `json:"cf_token,optional"` + } + // Check user is exist request + TelephoneCheckUserRequest { + Telephone string `form:"telephone" validate:"required"` + TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` + } + // User login response + TelephoneCheckUserResponse { + exist bool `json:"exist"` + } + // User login response + TelephoneRegisterRequest { + Telephone string `json:"telephone" validate:"required"` + TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` + Password string `json:"password" validate:"required"` + Invite string `json:"invite,optional"` + Code string `json:"code,optional"` + IP string `header:"X-Original-Forwarded-For"` + CfToken string `json:"cf_token,optional"` + } + // User login response + TelephoneResetPasswordRequest { + Telephone string `json:"telephone" validate:"required"` + TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` + Password string `json:"password" validate:"required"` + Code string `json:"code,optional"` + IP string `header:"X-Original-Forwarded-For"` + CfToken string `json:"cf_token,optional"` + } + AppleLoginCallbackRequest { + Code string `form:"code"` + IDToken string `form:"id_token"` + State string `form:"state"` + } + GoogleLoginCallbackRequest { + Code string `form:"code"` + State string `form:"state"` + } +) + +@server ( + prefix: v1/auth + group: auth +) +service ppanel { + @doc "User login" + @handler UserLogin + post /login (UserLoginRequest) returns (LoginResponse) + + @doc "Check user is exist" + @handler CheckUser + get /check (CheckUserRequest) returns (CheckUserResponse) + + @doc "User register" + @handler UserRegister + post /register (UserRegisterRequest) returns (LoginResponse) + + @doc "Reset password" + @handler ResetPassword + post /reset (ResetPasswordRequest) returns (LoginResponse) + + @doc "User Telephone login" + @handler TelephoneLogin + post /login/telephone (TelephoneLoginRequest) returns (LoginResponse) + + @doc "Check user telephone is exist" + @handler CheckUserTelephone + get /check/telephone (TelephoneCheckUserRequest) returns (TelephoneCheckUserResponse) + + @doc "User Telephone register" + @handler TelephoneUserRegister + post /register/telephone (TelephoneRegisterRequest) returns (LoginResponse) + + @doc "Reset password" + @handler TelephoneResetPassword + post /reset/telephone (TelephoneResetPasswordRequest) returns (LoginResponse) +} + +@server ( + prefix: v1/auth/oauth + group: auth/oauth +) +service ppanel { + @doc "OAuth login" + @handler OAuthLogin + post /login (OAthLoginRequest) returns (OAuthLoginResponse) + + @doc "OAuth login get token" + @handler OAuthLoginGetToken + post /login/token (OAuthLoginGetTokenRequest) returns (LoginResponse) + + @doc "Apple Login Callback" + @handler AppleLoginCallback + post /callback/apple (AppleLoginCallbackRequest) +} + diff --git a/apis/common.api b/apis/common.api new file mode 100644 index 0000000..545c3c9 --- /dev/null +++ b/apis/common.api @@ -0,0 +1,125 @@ +syntax = "v1" + +info ( + title: "Common API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "./types.api" + +type ( + VeifyConfig { + TurnstileSiteKey string `json:"turnstile_site_key"` + EnableLoginVerify bool `json:"enable_login_verify"` + EnableRegisterVerify bool `json:"enable_register_verify"` + EnableResetPasswordVerify bool `json:"enable_reset_password_verify"` + } + GetGlobalConfigResponse { + Site SiteConfig `json:"site"` + Verify VeifyConfig `json:"verify"` + Auth AuthConfig `json:"auth"` + Invite InviteConfig `json:"invite"` + Currency Currency `json:"currency"` + Subscribe SubscribeConfig `json:"subscribe"` + VerifyCode PubilcVerifyCodeConfig `json:"verify_code"` + OAuthMethods []string `json:"oauth_methods"` + WebAd bool `json:"web_ad"` + } + Currency { + CurrencyUnit string `json:"currency_unit"` + CurrencySymbol string `json:"currency_symbol"` + } + GetTosResponse { + TosContent string `json:"tos_content"` + } + GetAppcationResponse { + Config ApplicationConfig `json:"config"` + Applications []ApplicationResponseInfo `json:"applications"` + } + // GetCodeRequest Get code request + SendCodeRequest { + Email string `json:"email" validate:"required"` + Type uint8 `json:"type" validate:"required"` + } + SendSmsCodeRequest { + Type uint8 `json:"type" validate:"required"` + Telephone string `json:"telephone" validate:"required"` + TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` + } + // GetCodeResponse Get code response + SendCodeResponse { + Code string `json:"code,omitempty"` + Status bool `json:"status"` + } + // GetStatResponse Get stat response + GetStatResponse { + User int64 `json:"user"` + Node int64 `json:"node"` + Country int64 `json:"country"` + Protocol []string `json:"protocol"` + } + // Get ads + GetAdsRequest { + Device string `form:"device"` + Position string `form:"position"` + } + GetAdsResponse { + List []Ads `json:"list"` + } + CheckVerificationCodeRequest { + Method string `json:"method" validate:"required,oneof=email mobile"` + Account string `json:"account" validate:"required"` + Code string `json:"code" validate:"required"` + Type uint8 `json:"type" validate:"required"` + } + + CheckVerificationCodeRespone{ + Status bool `json:"status"` + } +) + +@server ( + prefix: v1/common + group: common +) +service ppanel { + @doc "Get global config" + @handler GetGlobalConfig + get /site/config returns (GetGlobalConfigResponse) + + @doc "Get Tos Content" + @handler GetApplication + get /application returns (GetAppcationResponse) + + @doc "Get Tos Content" + @handler GetTos + get /site/tos returns (GetTosResponse) + + @doc "Get Privacy Policy" + @handler GetPrivacyPolicy + get /site/privacy returns (PrivacyPolicyConfig) + + @doc "Get stat" + @handler GetStat + get /site/stat returns (GetStatResponse) + + @doc "Get verification code" + @handler SendEmailCode + post /send_code (SendCodeRequest) returns (SendCodeResponse) + + @doc "Get sms verification code" + @handler SendSmsCode + post /send_sms_code (SendSmsCodeRequest) returns (SendCodeResponse) + + @doc "Get Ads" + @handler GetAds + get /ads (GetAdsRequest) returns (GetAdsResponse) + + @doc "Check verification code" + @handler CheckVerificationCode + post /check_verification_code (CheckVerificationCodeRequest) returns (CheckVerificationCodeRespone) +} + diff --git a/apis/node/node.api b/apis/node/node.api new file mode 100644 index 0000000..8195854 --- /dev/null +++ b/apis/node/node.api @@ -0,0 +1,121 @@ +syntax = "v1" + +info ( + title: "Node API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +type ( + ShadowsocksProtocol { + Port int `json:"port"` + Method string `json:"method"` + } + VmessProtocol { + Host string `json:"host"` + Port int `json:"port"` + EnableTLS *bool `json:"enable_tls"` + TLSConfig string `json:"tls_config"` + Network string `json:"network"` + Transport string `json:"transport"` + } + VlessProtocol { + Host string `json:"host"` + Port int `json:"port"` + Network string `json:"network"` + Transport string `json:"transport"` + Security string `json:"security"` + SecurityConfig string `json:"security_config"` + XTLS string `json:"xtls"` + } + TrojanProtocol { + Host string `json:"host"` + Port int `json:"port"` + EnableTLS *bool `json:"enable_tls"` + TLSConfig string `json:"tls_config"` + Network string `json:"network"` + Transport string `json:"transport"` + } + GetServerConfigRequest { + ServerCommon + } + GetServerConfigResponse { + Basic ServerBasic `json:"basic"` + Protocol string `json:"protocol"` + Config interface{} `json:"config"` + } + ServerBasic { + PushInterval int64 `json:"push_interval"` + PullInterval int64 `json:"pull_interval"` + } + ServerCommon { + Protocol string `form:"protocol"` + ServerId int64 `form:"server_id"` + SecretKey string `form:"secret_key"` + } + ServerUser { + Id int64 `json:"id"` + UUID string `json:"uuid"` + SpeedLimit int64 `json:"speed_limit"` + DeviceLimit int64 `json:"device_limit"` + } + GetServerUserListRequest { + ServerCommon + } + GetServerUserListResponse { + Users []ServerUser `json:"users"` + } + UserTraffic { + SID int64 `json:"uid"` + Upload int64 `json:"upload"` + Download int64 `json:"download"` + } + ServerPushUserTrafficRequest { + ServerCommon + Traffic []UserTraffic `json:"traffic"` + } + + ServerPushStatusRequest { + ServerCommon + Cpu float64 `json:"cpu"` + Mem float64 `json:"mem"` + Disk float64 `json:"disk"` + UpdatedAt int64 `json:"updated_at"` + } + OnlineUsersRequest { + ServerCommon + Users []OnlineUser `json:"users"` + } +) + +@server ( + prefix: v1/server + group: server + middleware: ServerMiddleware +) +service ppanel { + @doc "Get user list" + @handler GetServerUserList + get /user (GetServerUserListRequest) returns (GetServerUserListResponse) + + @doc "Push user Traffic" + @handler ServerPushUserTraffic + post /push (ServerPushUserTrafficRequest) + + @doc "Push server status" + @handler ServerPushStatus + post /status (ServerPushStatusRequest) + + @doc "Get server config" + @handler GetServerConfig + get /config (GetServerConfigRequest) returns (GetServerConfigResponse) + + @doc "Push online users" + @handler PushOnlineUsers + post /online (OnlineUsersRequest) +} + diff --git a/apis/public/announcement.api b/apis/public/announcement.api new file mode 100644 index 0000000..664ac4c --- /dev/null +++ b/apis/public/announcement.api @@ -0,0 +1,24 @@ +syntax = "v1" + +info ( + title: "Announcement API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + + +@server ( + prefix: v1/public/announcement + group: public/announcement + middleware: AuthMiddleware +) +service ppanel { + @doc "Query announcement" + @handler QueryAnnouncement + get /list (QueryAnnouncementRequest) returns (QueryAnnouncementResponse) +} + diff --git a/apis/public/document.api b/apis/public/document.api new file mode 100644 index 0000000..41d7291 --- /dev/null +++ b/apis/public/document.api @@ -0,0 +1,28 @@ +syntax = "v1" + +info( + title: "Document API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + + +@server ( + prefix: v1/public/document + group: public/document + middleware: AuthMiddleware +) + +service ppanel { + @doc "Get document list" + @handler QueryDocumentList + get /list returns (QueryDocumentListResponse) + + @doc "Get document detail" + @handler QueryDocumentDetail + get /detail (QueryDocumentDetailRequest) returns (Document) +} \ No newline at end of file diff --git a/apis/public/order.api b/apis/public/order.api new file mode 100644 index 0000000..0db556f --- /dev/null +++ b/apis/public/order.api @@ -0,0 +1,51 @@ +syntax = "v1" + +info ( + title: "Order API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +@server ( + prefix: v1/public/order + group: public/order + middleware: AuthMiddleware +) +service ppanel { + @doc "Pre create order" + @handler PreCreateOrder + post /pre (PurchaseOrderRequest) returns (PreOrderResponse) + + @doc "purchase Subscription" + @handler Purchase + post /purchase (PurchaseOrderRequest) returns (PurchaseOrderResponse) + + @doc "Renewal Subscription" + @handler Renewal + post /renewal (RenewalOrderRequest) returns (RenewalOrderResponse) + + @doc "Reset traffic" + @handler ResetTraffic + post /reset (ResetTrafficOrderRequest) returns (ResetTrafficOrderResponse) + + @doc "Recharge" + @handler Recharge + post /recharge (RechargeOrderRequest) returns (RechargeOrderResponse) + + @doc "Close order" + @handler CloseOrder + post /close (CloseOrderRequest) + + @doc "Get order" + @handler QueryOrderDetail + get /detail (QueryOrderDetailRequest) returns (OrderDetail) + + @doc "Get order list" + @handler QueryOrderList + get /list (QueryOrderListRequest) returns (QueryOrderListResponse) +} + diff --git a/apis/public/payment.api b/apis/public/payment.api new file mode 100644 index 0000000..247f9d4 --- /dev/null +++ b/apis/public/payment.api @@ -0,0 +1,24 @@ +syntax = "v1" + +info ( + title: "payment API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + + +@server ( + prefix: v1/public/payment + group: public/payment + middleware: AuthMiddleware +) +service ppanel { + @doc "Get available payment methods" + @handler GetAvailablePaymentMethods + get /methods returns (GetAvailablePaymentMethodsResponse) +} + diff --git a/apis/public/portal.api b/apis/public/portal.api new file mode 100644 index 0000000..709e48b --- /dev/null +++ b/apis/public/portal.api @@ -0,0 +1,95 @@ +syntax = "v1" + +info ( + title: "Portal API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +type ( + PortalPurchaseRequest { + AuthType string `json:"auth_type"` + Identifier string `json:"identifier"` + Password string `json:"password,omitempty"` + Payment int64 `json:"payment"` + SubscribeId int64 `json:"subscribe_id"` + Quantity int64 `json:"quantity"` + Coupon string `json:"coupon,omitempty"` + TurnstileToken string `json:"turnstile_token,omitempty"` + } + PortalPurchaseResponse { + OrderNo string `json:"order_no"` + } + GetSubscriptionResponse { + List []Subscribe `json:"list"` + } + PrePurchaseOrderRequest { + Payment int64 `json:"payment,omitempty"` + SubscribeId int64 `json:"subscribe_id"` + Quantity int64 `json:"quantity"` + Coupon string `json:"coupon,omitempty"` + } + PrePurchaseOrderResponse { + Price int64 `json:"price"` + Amount int64 `json:"amount"` + Discount int64 `json:"discount"` + Coupon string `json:"coupon"` + CouponDiscount int64 `json:"coupon_discount"` + FeeAmount int64 `json:"fee_amount"` + } + QueryPurchaseOrderRequest { + AuthType string `form:"auth_type"` + Identifier string `form:"identifier"` + OrderNo string `form:"order_no"` + } + QueryPurchaseOrderResponse { + OrderNo string `json:"order_no"` + Subscribe Subscribe `json:"subscribe"` + Quantity int64 `json:"quantity"` + Price int64 `json:"price"` + Amount int64 `json:"amount"` + Discount int64 `json:"discount"` + Coupon string `json:"coupon"` + CouponDiscount int64 `json:"coupon_discount"` + FeeAmount int64 `json:"fee_amount"` + Payment PaymentMethod `json:"payment"` + Status uint8 `json:"status"` + CreatedAt int64 `json:"created_at"` + Token string `json:"token,omitempty"` + } +) + +@server ( + prefix: v1/public/portal + group: public/portal +) +service ppanel { + @doc "Get available payment methods" + @handler GetAvailablePaymentMethods + get /payment-method returns (GetAvailablePaymentMethodsResponse) + + @doc "Get Subscription" + @handler GetSubscription + get /subscribe returns (GetSubscriptionResponse) + + @doc "Pre Purchase Order" + @handler PrePurchaseOrder + post /pre (PrePurchaseOrderRequest) returns (PrePurchaseOrderResponse) + + @doc "Purchase subscription" + @handler Purchase + post /purchase (PortalPurchaseRequest) returns (PortalPurchaseResponse) + + @doc "Query Purchase Order" + @handler QueryPurchaseOrder + get /order/status (QueryPurchaseOrderRequest) returns (QueryPurchaseOrderResponse) + + @doc "Purchase Checkout" + @handler PurchaseCheckout + post /order/checkout (CheckoutOrderRequest) returns (CheckoutOrderResponse) +} + diff --git a/apis/public/subscribe.api b/apis/public/subscribe.api new file mode 100644 index 0000000..98fd2c2 --- /dev/null +++ b/apis/public/subscribe.api @@ -0,0 +1,31 @@ +syntax = "v1" + +info ( + title: "Subscribe API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +@server ( + prefix: v1/public/subscribe + group: public/subscribe + middleware: AuthMiddleware +) +service ppanel { + @doc "Get subscribe list" + @handler QuerySubscribeList + get /list returns (QuerySubscribeListResponse) + + @doc "Get subscribe group list" + @handler QuerySubscribeGroupList + get /group/list returns (QuerySubscribeGroupListResponse) + + @doc "Get application config" + @handler QueryApplicationConfig + get /application/config returns (ApplicationResponse) +} + diff --git a/apis/public/ticket.api b/apis/public/ticket.api new file mode 100644 index 0000000..b5a846b --- /dev/null +++ b/apis/public/ticket.api @@ -0,0 +1,70 @@ +syntax = "v1" + +info ( + title: "Ticket API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +type ( + + GetUserTicketListResponse { + Total int64 `json:"total"` + List []Ticket `json:"list"` + } + CreateUserTicketRequest { + Title string `json:"title"` + Description string `json:"description"` + } + GetUserTicketListRequest { + Page int `form:"page"` + Size int `form:"size"` + Status *uint8 `form:"status,omitempty"` + Search string `form:"search,omitempty"` + } + GetUserTicketDetailRequest { + Id int64 `form:"id" validate:"required"` + } + UpdateUserTicketStatusRequest { + Id int64 `json:"id" validate:"required"` + Status *uint8 `json:"status" validate:"required"` + } + CreateUserTicketFollowRequest { + TicketId int64 `json:"ticket_id"` + From string `json:"from"` + Type uint8 `json:"type"` + Content string `json:"content"` + } +) + +@server ( + prefix: v1/public/ticket + group: public/ticket + middleware: AuthMiddleware +) +service ppanel { + @doc "Get ticket list" + @handler GetUserTicketList + get /list (GetUserTicketListRequest) returns (GetUserTicketListResponse) + + @doc "Get ticket detail" + @handler GetUserTicketDetails + get /detail (GetUserTicketDetailRequest) returns (Ticket) + + @doc "Update ticket status" + @handler UpdateUserTicketStatus + put / (UpdateUserTicketStatusRequest) + + @doc "Create ticket follow" + @handler CreateUserTicketFollow + post /follow (CreateUserTicketFollowRequest) + + @doc "Create ticket" + @handler CreateUserTicket + post / (CreateUserTicketRequest) +} + diff --git a/apis/public/user.api b/apis/public/user.api new file mode 100644 index 0000000..a2c5a53 --- /dev/null +++ b/apis/public/user.api @@ -0,0 +1,211 @@ +syntax = "v1" + +info ( + title: "User API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +type ( + UpdateUserNotifyRequest { + EnableBalanceNotify *bool `json:"enable_balance_notify"` + EnableLoginNotify *bool `json:"enable_login_notify"` + EnableSubscribeNotify *bool `json:"enable_subscribe_notify"` + EnableTradeNotify *bool `json:"enable_trade_notify"` + } + UpdateUserPasswordRequest { + Password string `json:"password" validate:"required"` + } + QueryUserSubscribeListResponse { + List []UserSubscribe `json:"list"` + Total int64 `json:"total"` + } + QueryUserBalanceLogListResponse { + List []UserBalanceLog `json:"list"` + Total int64 `json:"total"` + } + + CommissionLog { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + OrderNo string `json:"order_no"` + Amount int64 `json:"amount"` + CreatedAt int64 `json:"created_at"` + } + QueryUserCommissionLogListRequest { + Page int `form:"page"` + Size int `form:"size"` + } + QueryUserCommissionLogListResponse { + List []CommissionLog `json:"list"` + Total int64 `json:"total"` + } + BindTelegramResponse { + Url string `json:"url"` + ExpiredAt int64 `json:"expired_at"` + } + PreUnsubscribeRequest { + Id int64 `json:"id"` + } + PreUnsubscribeResponse { + DeductionAmount int64 `json:"deduction_amount"` + } + UnsubscribeRequest { + Id int64 `json:"id"` + } + BindOAuthRequest { + Method string `json:"method"` + Redirect string `json:"redirect"` + } + BindOAuthResponse { + Redirect string `json:"redirect"` + } + BindOAuthCallbackRequest { + Method string `json:"method"` + Callback interface{} `json:"callback"` + } + GetOAuthMethodsResponse { + Methods []UserAuthMethod `json:"methods"` + } + UnbindOAuthRequest { + Method string `json:"method"` + } + ResetUserSubscribeTokenRequest { + UserSubscribeId int64 `json:"user_subscribe_id"` + } + + GetLoginLogRequest { + Page int `form:"page"` + Size int `form:"size"` + } + + GetLoginLogResponse { + List []UserLoginLog `json:"list"` + Total int64 `json:"total"` + } + + GetSubscribeLogRequest { + Page int `form:"page"` + Size int `form:"size"` + } + + GetSubscribeLogResponse { + List []UserSubscribeLog `json:"list"` + Total int64 `json:"total"` + } + + UpdateBindMobileRequest{ + AreaCode string `json:"area_code" validate:"required"` + Mobile string `json:"mobile" validate:"required"` + Code string `json:"code" validate:"required"` + } + + UpdateBindEmailRequest{ + Email string `json:"email" validate:"required"` + } + + VerifyEmailRequest { + Email string `json:"email" validate:"required"` + Code string `json:"code" validate:"required"` + } +) + +@server ( + prefix: v1/public/user + group: public/user + middleware: AuthMiddleware +) +service ppanel { + @doc "Query User Info" + @handler QueryUserInfo + get /info returns (User) + + @doc "Update User Notify" + @handler UpdateUserNotify + put /notify (UpdateUserNotifyRequest) + + @doc "Update User Password" + @handler UpdateUserPassword + put /password (UpdateUserPasswordRequest) + + @doc "Query User Subscribe" + @handler QueryUserSubscribe + get /subscribe returns (QueryUserSubscribeListResponse) + + @doc "Pre Unsubscribe" + @handler PreUnsubscribe + post /unsubscribe/pre (PreUnsubscribeRequest) returns (PreUnsubscribeResponse) + + @doc "Unsubscribe" + @handler Unsubscribe + post /unsubscribe (UnsubscribeRequest) + + @doc "Query User Balance Log" + @handler QueryUserBalanceLog + get /balance_log returns (QueryUserBalanceLogListResponse) + + @doc "Query User Affiliate Count" + @handler QueryUserAffiliate + get /affiliate/count returns (QueryUserAffiliateCountResponse) + + @doc "Query User Affiliate List" + @handler QueryUserAffiliateList + get /affiliate/list (QueryUserAffiliateListRequest) returns (QueryUserAffiliateListResponse) + + @doc "Bind Telegram" + @handler BindTelegram + get /bind_telegram returns (BindTelegramResponse) + + @doc "Unbind Telegram" + @handler UnbindTelegram + post /unbind_telegram + + @doc "Query User Commission Log" + @handler QueryUserCommissionLog + get /commission_log (QueryUserCommissionLogListRequest) returns (QueryUserCommissionLogListResponse) + + @doc "Bind OAuth" + @handler BindOAuth + post /bind_oauth (BindOAuthRequest) returns (BindOAuthResponse) + + @doc "Bind OAuth Callback" + @handler BindOAuthCallback + post /bind_oauth/callback (BindOAuthCallbackRequest) + + @doc "Get OAuth Methods" + @handler GetOAuthMethods + get /oauth_methods returns (GetOAuthMethodsResponse) + + @doc "Unbind OAuth" + @handler UnbindOAuth + post /unbind_oauth (UnbindOAuthRequest) + + @doc "Reset User Subscribe Token" + @handler ResetUserSubscribeToken + put /subscribe_token (ResetUserSubscribeTokenRequest) + + @doc "Get Login Log" + @handler GetLoginLog + get /login_log (GetLoginLogRequest) returns (GetLoginLogResponse) + + @doc "Get Subscribe Log" + @handler GetSubscribeLog + get /subscribe_log (GetSubscribeLogRequest) returns (GetSubscribeLogResponse) + + @doc "Verify Email" + @handler VerifyEmail + post /verify_email (VerifyEmailRequest) + + @doc "Update Bind Mobile" + @handler UpdateBindMobile + put /bind_mobile (UpdateBindMobileRequest) + + @doc "Update Bind Email" + @handler UpdateBindEmail + put /bind_email (UpdateBindEmailRequest) +} + diff --git a/apis/swagger_admin.api b/apis/swagger_admin.api new file mode 100644 index 0000000..c56b302 --- /dev/null +++ b/apis/swagger_admin.api @@ -0,0 +1,26 @@ +syntax = "v1" + +info( + title: "admin API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) +import ( + "./admin/system.api" + "./admin/user.api" + "./admin/server.api" + "./admin/subscribe.api" + "./admin/payment.api" + "./admin/coupon.api" + "./admin/order.api" + "./admin/ticket.api" + "./admin/announcement.api" + "./admin/document.api" + "./admin/tool.api" + "./admin/console.api" + "./admin/auth.api" + "./admin/log.api" + "./admin/ads.api" +) \ No newline at end of file diff --git a/apis/swagger_app.api b/apis/swagger_app.api new file mode 100644 index 0000000..c4ff674 --- /dev/null +++ b/apis/swagger_app.api @@ -0,0 +1,21 @@ +syntax = "v1" + +info( + title: "App API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import ( + "./app/auth.api" + "./app/user.api" + "./app/node.api" + "./app/ws.api" + "./app/order.api" + "./app/announcement.api" + "./app/payment.api" + "./app/document.api" + "./app/subscribe.api" +) \ No newline at end of file diff --git a/apis/swagger_common.api b/apis/swagger_common.api new file mode 100644 index 0000000..518b655 --- /dev/null +++ b/apis/swagger_common.api @@ -0,0 +1,14 @@ +syntax = "v1" + +info( + title: "common API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import ( + "./common.api" + "./auth/auth.api" +) \ No newline at end of file diff --git a/apis/swagger_node.api b/apis/swagger_node.api new file mode 100644 index 0000000..6a2ec35 --- /dev/null +++ b/apis/swagger_node.api @@ -0,0 +1,11 @@ +syntax = "v1" + +info( + title: "Node API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "./node/node.api" \ No newline at end of file diff --git a/apis/swagger_user.api b/apis/swagger_user.api new file mode 100644 index 0000000..bd27b82 --- /dev/null +++ b/apis/swagger_user.api @@ -0,0 +1,19 @@ +syntax = "v1" + +info( + title: "User API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) +import ( + "./public/user.api" + "./public/subscribe.api" + "./public/order.api" + "./public/announcement.api" + "./public/ticket.api" + "./public/payment.api" + "./public/document.api" + "./public/portal.api" +) \ No newline at end of file diff --git a/apis/types.api b/apis/types.api new file mode 100644 index 0000000..5d3d9a5 --- /dev/null +++ b/apis/types.api @@ -0,0 +1,752 @@ +syntax = "v1" + +info ( + title: "Interface Model" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +type ( + User { + Id int64 `json:"id"` + Avatar string `json:"avatar"` + Balance int64 `json:"balance"` + Commission int64 `json:"commission"` + GiftAmount int64 `json:"gift_amount"` + Telegram int64 `json:"telegram"` + ReferCode string `json:"refer_code"` + RefererId int64 `json:"referer_id"` + Enable bool `json:"enable"` + IsAdmin bool `json:"is_admin,omitempty"` + EnableBalanceNotify bool `json:"enable_balance_notify"` + EnableLoginNotify bool `json:"enable_login_notify"` + EnableSubscribeNotify bool `json:"enable_subscribe_notify"` + EnableTradeNotify bool `json:"enable_trade_notify"` + AuthMethods []UserAuthMethod `json:"auth_methods"` + UserDevices []UserDevice `json:"user_devices"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + DeletedAt int64 `json:"deleted_at,omitempty"` + IsDel bool `json:"is_del,omitempty"` + } + Follow { + Id int64 `json:"id"` + TicketId int64 `json:"ticket_id"` + From string `json:"from"` + Type uint8 `json:"type"` + Content string `json:"content"` + CreatedAt int64 `json:"created_at"` + } + Ticket { + Id int64 `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + UserId int64 `json:"user_id"` + Follows []Follow `json:"follow,omitempty"` + Status uint8 `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + SiteConfig { + Host string `json:"host"` + SiteName string `json:"site_name"` + SiteDesc string `json:"site_desc"` + SiteLogo string `json:"site_logo"` + Keywords string `json:"keywords"` + CustomHTML string `json:"custom_html"` + CustomData string `json:"custom_data"` + } + SubscribeConfig { + SingleModel bool `json:"single_model"` + SubscribePath string `json:"subscribe_path"` + SubscribeDomain string `json:"subscribe_domain"` + PanDomain bool `json:"pan_domain"` + } + VerifyCodeConfig { + VerifyCodeExpireTime int64 `json:"verify_code_expire_time"` + VerifyCodeLimit int64 `json:"verify_code_limit"` + VerifyCodeInterval int64 `json:"verify_code_interval"` + } + PubilcVerifyCodeConfig { + VerifyCodeInterval int64 `json:"verify_code_interval"` + } + SubscribeType { + SubscribeTypes []string `json:"subscribe_types"` + } + Application { + Id int64 `json:"id"` + Icon string `json:"icon"` + Name string `json:"name"` + Description string `json:"description"` + SubscribeType string `json:"subscribe_type"` + } + ApplicationVersion { + Id int64 `json:"id"` + Url string `json:"url"` + Version string `json:"version" validate:"required"` + Description string `json:"description"` + IsDefault bool `json:"is_default"` + } + ApplicationResponse { + Applications []ApplicationResponseInfo `json:"applications"` + } + ApplicationResponseInfo { + Id int64 `json:"id"` + Name string `json:"name"` + Icon string `json:"icon"` + Description string `json:"description"` + SubscribeType string `json:"subscribe_type"` + Platform ApplicationPlatform `json:"platform"` + } + ApplicationPlatform { + IOS []*ApplicationVersion `json:"ios,omitempty"` + MacOS []*ApplicationVersion `json:"macos,omitempty"` + Linux []*ApplicationVersion `json:"linux,omitempty"` + Android []*ApplicationVersion `json:"android,omitempty"` + Windows []*ApplicationVersion `json:"windows,omitempty"` + Harmony []*ApplicationVersion `json:"harmony,omitempty"` + } + AuthConfig { + Mobile MobileAuthenticateConfig `json:"mobile"` + Email EmailAuthticateConfig `json:"email"` + Register PubilcRegisterConfig `json:"register"` + } + PubilcRegisterConfig { + StopRegister bool `json:"stop_register"` + EnableIpRegisterLimit bool `json:"enable_ip_register_limit"` + IpRegisterLimit int64 `json:"ip_register_limit"` + IpRegisterLimitDuration int64 `json:"ip_register_limit_duration"` + } + MobileAuthenticateConfig { + Enable bool `json:"enable"` + EnableWhitelist bool `json:"enable_whitelist"` + Whitelist []string `json:"whitelist"` + } + EmailAuthticateConfig { + Enable bool `json:"enable"` + EnableVerify bool `json:"enable_verify"` + EnableDomainSuffix bool `json:"enable_domain_suffix"` + DomainSuffixList string `json:"domain_suffix_list"` + } + RegisterConfig { + StopRegister bool `json:"stop_register"` + EnableTrial bool `json:"enable_trial"` + TrialSubscribe int64 `json:"trial_subscribe"` + TrialTime int64 `json:"trial_time"` + TrialTimeUnit string `json:"trial_time_unit"` + EnableIpRegisterLimit bool `json:"enable_ip_register_limit"` + IpRegisterLimit int64 `json:"ip_register_limit"` + IpRegisterLimitDuration int64 `json:"ip_register_limit_duration"` + } + VerifyConfig { + TurnstileSiteKey string `json:"turnstile_site_key"` + TurnstileSecret string `json:"turnstile_secret"` + EnableLoginVerify bool `json:"enable_login_verify"` + EnableRegisterVerify bool `json:"enable_register_verify"` + EnableResetPasswordVerify bool `json:"enable_reset_password_verify"` + } + NodeConfig { + NodeSecret string `json:"node_secret"` + NodePullInterval int64 `json:"node_pull_interval"` + NodePushInterval int64 `json:"node_push_interval"` + } + InviteConfig { + ForcedInvite bool `json:"forced_invite"` + ReferralPercentage int64 `json:"referral_percentage"` + OnlyFirstPurchase bool `json:"only_first_purchase"` + } + TelegramConfig { + TelegramBotToken string `json:"telegram_bot_token"` + TelegramGroupUrl string `json:"telegram_group_url"` + TelegramNotify bool `json:"telegram_notify"` + TelegramWebHookDomain string `json:"telegram_web_hook_domain"` + } + TosConfig { + TosContent string `json:"tos_content"` + } + PrivacyPolicyConfig { + PrivacyPolicy string `json:"privacy_policy"` + } + CurrencyConfig { + AccessKey string `json:"access_key"` + CurrencyUnit string `json:"currency_unit"` + CurrencySymbol string `json:"currency_symbol"` + } + SubscribeDiscount { + Quantity int64 `json:"quantity"` + Discount int64 `json:"discount"` + } + Subscribe { + Id int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + UnitPrice int64 `json:"unit_price"` + UnitTime string `json:"unit_time"` + Discount []SubscribeDiscount `json:"discount"` + Replacement int64 `json:"replacement"` + Inventory int64 `json:"inventory"` + Traffic int64 `json:"traffic"` + SpeedLimit int64 `json:"speed_limit"` + DeviceLimit int64 `json:"device_limit"` + Quota int64 `json:"quota"` + GroupId int64 `json:"group_id"` + ServerGroup []int64 `json:"server_group"` + Server []int64 `json:"server"` + Show bool `json:"show"` + Sell bool `json:"sell"` + Sort int64 `json:"sort"` + DeductionRatio int64 `json:"deduction_ratio"` + AllowDeduction bool `json:"allow_deduction"` + ResetCycle int64 `json:"reset_cycle"` + RenewalReset bool `json:"renewal_reset"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + SubscribeGroup { + Id int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + Shadowsocks { + Method string `json:"method" validate:"required"` + Port int `json:"port" validate:"required"` + ServerKey string `json:"server_key"` + } + Vmess { + Port int `json:"port" validate:"required"` + Transport string `json:"transport" validate:"required"` + TransportConfig TransportConfig `json:"transport_config"` + Security string `json:"security" validate:"required"` + SecurityConfig SecurityConfig `json:"security_config"` + } + Vless { + Port int `json:"port" validate:"required"` + Flow string `json:"flow" validate:"required"` + Transport string `json:"transport" validate:"required"` + TransportConfig TransportConfig `json:"transport_config"` + Security string `json:"security" validate:"required"` + SecurityConfig SecurityConfig `json:"security_config"` + } + Trojan { + Port int `json:"port" validate:"required"` + Transport string `json:"transport" validate:"required"` + TransportConfig TransportConfig `json:"transport_config"` + Security string `json:"security" validate:"required"` + SecurityConfig SecurityConfig `json:"security_config"` + } + Hysteria2 { + Port int `json:"port" validate:"required"` + HopPorts string `json:"hop_ports" validate:"required"` + HopInterval int `json:"hop_interval" validate:"required"` + ObfsPassword string `json:"obfs_password" validate:"required"` + SecurityConfig SecurityConfig `json:"security_config"` + } + Tuic { + Port int `json:"port" validate:"required"` + SecurityConfig SecurityConfig `json:"security_config"` + } + SecurityConfig { + SNI string `json:"sni"` + AllowInsecure *bool `json:"allow_insecure"` + Fingerprint string `json:"fingerprint"` + RealityServerAddr string `json:"reality_server_addr"` + RealityServerPort int `json:"reality_server_port"` + RealityPrivateKey string `json:"reality_private_key"` + RealityPublicKey string `json:"reality_public_key"` + RealityShortId string `json:"reality_short_id"` + } + TransportConfig { + Path string `json:"path"` + Host string `json:"host"` + ServiceName string `json:"service_name"` + } + Server { + Id int64 `json:"id"` + Tags []string `json:"tags"` + Country string `json:"country"` + City string `json:"city"` + Name string `json:"name"` + ServerAddr string `json:"server_addr"` + RelayMode string `json:"relay_mode"` + RelayNode []NodeRelay `json:"relay_node"` + SpeedLimit int `json:"speed_limit"` + TrafficRatio float32 `json:"traffic_ratio"` + GroupId int64 `json:"group_id"` + Protocol string `json:"protocol"` + Config interface{} `json:"config"` + Enable *bool `json:"enable"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + Status *NodeStatus `json:"status"` + Sort int64 `json:"sort"` + } + OnlineUser { + SID int64 `json:"uid"` + IP string `json:"ip"` + } + NodeStatus { + Online interface{} `json:"online"` + Cpu float64 `json:"cpu"` + Mem float64 `json:"mem"` + Disk float64 `json:"disk"` + UpdatedAt int64 `json:"updated_at"` + } + ServerGroup { + Id int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + PaymentMethod { + Id int64 `json:"id"` + Name string `json:"name"` + Platform string `json:"platform"` + Description string `json:"description"` + Icon string `json:"icon"` + FeeMode uint `json:"fee_mode"` + FeePercent int64 `json:"fee_percent"` + FeeAmount int64 `json:"fee_amount"` + } + PaymentConfig { + Id int64 `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` + Platform string `json:"platform" validate:"required"` + Description string `json:"description"` + Icon string `json:"icon,omitempty"` + Domain string `json:"domain,omitempty"` + Config interface{} `json:"config" validate:"required"` + FeeMode uint `json:"fee_mode"` + FeePercent int64 `json:"fee_percent,omitempty"` + FeeAmount int64 `json:"fee_amount,omitempty"` + Enable *bool `json:"enable" validate:"required"` + } + PaymentMethodDetail { + Id int64 `json:"id"` + Name string `json:"name"` + Platform string `json:"platform"` + Description string `json:"description"` + Icon string `json:"icon"` + Domain string `json:"domain"` + Config interface{} `json:"config"` + FeeMode uint `json:"fee_mode"` + FeePercent int64 `json:"fee_percent"` + FeeAmount int64 `json:"fee_amount"` + Enable bool `json:"enable"` + NotifyURL string `json:"notify_url"` + } + Order { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + OrderNo string `json:"order_no"` + Type uint8 `json:"type"` + Quantity int64 `json:"quantity"` + Price int64 `json:"price"` + Amount int64 `json:"amount"` + GiftAmount int64 `json:"gift_amount"` + Discount int64 `json:"discount"` + Coupon string `json:"coupon"` + CouponDiscount int64 `json:"coupon_discount"` + Commission int64 `json:"commission,omitempty"` + Payment PaymentMethod `json:"payment"` + FeeAmount int64 `json:"fee_amount"` + TradeNo string `json:"trade_no"` + Status uint8 `json:"status"` + SubscribeId int64 `json:"subscribe_id"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + OrderDetail { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + OrderNo string `json:"order_no"` + Type uint8 `json:"type"` + Quantity int64 `json:"quantity"` + Price int64 `json:"price"` + Amount int64 `json:"amount"` + GiftAmount int64 `json:"gift_amount"` + Discount int64 `json:"discount"` + Coupon string `json:"coupon"` + CouponDiscount int64 `json:"coupon_discount"` + Commission int64 `json:"commission,omitempty"` + Payment PaymentMethod `json:"payment"` + Method string `json:"method"` + FeeAmount int64 `json:"fee_amount"` + TradeNo string `json:"trade_no"` + Status uint8 `json:"status"` + SubscribeId int64 `json:"subscribe_id"` + Subscribe Subscribe `json:"subscribe"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + Document { + Id int64 `json:"id"` + Title string `json:"title"` + Content string `json:"content"` + Tags []string `json:"tags"` + Show bool `json:"show"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + Coupon { + Id int64 `json:"id"` + Name string `json:"name"` + Code string `json:"code"` + Count int64 `json:"count"` + Type uint8 `json:"type"` + Discount int64 `json:"discount"` + StartTime int64 `json:"start_time"` + ExpireTime int64 `json:"expire_time"` + UserLimit int64 `json:"user_limit"` + Subscribe []int64 `json:"subscribe"` + UsedCount int64 `json:"used_count"` + Enable bool `json:"enable"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + Announcement { + Id int64 `json:"id"` + Title string `json:"title"` + Content string `json:"content"` + Show *bool `json:"show"` + Pinned *bool `json:"pinned"` + Popup *bool `json:"popup"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + UserSubscribe { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + OrderId int64 `json:"order_id"` + SubscribeId int64 `json:"subscribe_id"` + Subscribe Subscribe `json:"subscribe"` + StartTime int64 `json:"start_time"` + ExpireTime int64 `json:"expire_time"` + FinishedAt int64 `json:"finished_at"` + ResetTime int64 `json:"reset_time"` + Traffic int64 `json:"traffic"` + Download int64 `json:"download"` + Upload int64 `json:"upload"` + Token string `json:"token"` + Status uint8 `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + UserBalanceLog { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + Amount int64 `json:"amount"` + Type uint8 `json:"type"` + OrderId int64 `json:"order_id"` + Balance int64 `json:"balance"` + CreatedAt int64 `json:"created_at"` + } + UserAffiliate { + Avatar string `json:"avatar"` + Identifier string `json:"identifier"` + RegisteredAt int64 `json:"registered_at"` + Enable bool `json:"enable"` + } + SortItem { + Id int64 `json:"id" validate:"required"` + Sort int64 `json:"sort" validate:"required"` + } + TimePeriod { + StartTime string `json:"start_time"` + EndTime string `json:"end_time"` + Multiplier float32 `json:"multiplier"` + } + NodeRelay { + Host string `json:"host"` + Port int `json:"port"` + Prefix string `json:"prefix"` + } + ApplicationConfig { + AppId int64 `json:"app_id"` + EncryptionKey string `json:"encryption_key"` + EncryptionMethod string `json:"encryption_method"` + Domains []string `json:"domains" validate:"required"` + StartupPicture string `json:"startup_picture"` + StartupPictureSkipTime int64 `json:"startup_picture_skip_time"` + } + UserDevice { + Id int64 `json:"id"` + Ip string `json:"ip"` + Identifier string `json:"identifier"` + UserAgent string `json:"user_agent"` + Online bool `json:"online"` + Enabled bool `json:"enabled"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + UserAuthMethod { + AuthType string `json:"auth_type"` + AuthIdentifier string `json:"auth_identifier"` + Verified bool `json:"verified"` + } + AuthMethodConfig { + Id int64 `json:"id"` + Method string `json:"method"` + Config interface{} `json:"config"` + Enabled bool `json:"enabled"` + } + TrafficLog { + Id int64 `json:"id"` + ServerId int64 `json:"server_id"` + UserId int64 `json:"user_id"` + SubscribeId int64 `json:"subscribe_id"` + Download int64 `json:"download"` + Upload int64 `json:"upload"` + Timestamp int64 `json:"timestamp"` + } + ServerRuleGroup { + Id int64 `json:"id"` + Icon string `json:"icon"` + Name string `json:"name" validate:"required"` + Tags []string `json:"tags"` + Rules string `json:"rules"` + Enable bool `json:"enable"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + UserSubscribeLog { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + UserSubscribeId int64 `json:"user_subscribe_id"` + Token string `json:"token"` + IP string `json:"ip"` + UserAgent string `json:"user_agent"` + CreatedAt int64 `json:"created_at"` + } + UserLoginLog { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + LoginIP string `json:"login_ip"` + UserAgent string `json:"user_agent"` + Success bool `json:"success"` + CreatedAt int64 `json:"created_at"` + } + MessageLog { + Id int64 `json:"id"` + Type string `json:"type"` + Platform string `json:"platform"` + To string `json:"to"` + Subject string `json:"subject"` + Content string `json:"content"` + Status int `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + Ads { + Id int `json:"id"` + Title string `json:"title"` + Type string `json:"type"` + Content string `json:"content"` + Description string `json:"description"` + TargetURL string `json:"target_url"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + Status int `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + //public order + PurchaseOrderRequest { + SubscribeId int64 `json:"subscribe_id"` + Quantity int64 `json:"quantity"` + Payment int64 `json:"payment,omitempty"` + Coupon string `json:"coupon,omitempty"` + } + PreOrderResponse { + Price int64 `json:"price"` + Amount int64 `json:"amount"` + Discount int64 `json:"discount"` + GiftAmount int64 `json:"gift_amount"` + Coupon string `json:"coupon"` + CouponDiscount int64 `json:"coupon_discount"` + FeeAmount int64 `json:"fee_amount"` + } + PurchaseOrderResponse { + OrderNo string `json:"order_no"` + } + RenewalOrderRequest { + UserSubscribeID int64 `json:"user_subscribe_id"` + Quantity int64 `json:"quantity"` + Payment int64 `json:"payment"` + Coupon string `json:"coupon,omitempty"` + } + RenewalOrderResponse { + OrderNo string `json:"order_no"` + } + ResetTrafficOrderRequest { + UserSubscribeID int64 `json:"user_subscribe_id"` + Payment int64 `json:"payment"` + } + ResetTrafficOrderResponse { + OrderNo string `json:"order_no"` + } + RechargeOrderRequest { + Amount int64 `json:"amount"` + Payment int64 `json:"payment"` + } + RechargeOrderResponse { + OrderNo string `json:"order_no"` + } + PreRenewalOrderResponse { + OrderNo string `json:"orderNo"` + } + CloseOrderRequest { + OrderNo string `json:"orderNo" validate:"required"` + } + QueryOrderDetailRequest { + OrderNo string `form:"order_no" validate:"required"` + } + StripePayment { + Method string `json:"method"` + ClientSecret string `json:"client_secret"` + PublishableKey string `json:"publishable_key"` + } + QueryOrderListRequest { + Page int `form:"page" validate:"required"` + Size int `form:"size" validate:"required"` + } + QueryOrderListResponse { + Total int64 `json:"total"` + List []OrderDetail `json:"list"` + } + //public document + QueryDocumentListResponse { + Total int64 `json:"total"` + List []Document `json:"list"` + } + QueryDocumentDetailRequest { + Id int64 `form:"id" validate:"required"` + } + // public payment + GetAvailablePaymentMethodsResponse { + List []PaymentMethod `json:"list"` + } + // public announcement + QueryAnnouncementRequest { + Page int `form:"page"` + Size int `form:"size"` + Pinned *bool `form:"pinned"` + Popup *bool `form:"popup"` + } + QueryAnnouncementResponse { + Total int64 `json:"total"` + List []Announcement `json:"announcements"` + } + //subscribe + QuerySubscribeListResponse { + List []Subscribe `json:"list"` + Total int64 `json:"total"` + } + QuerySubscribeGroupListResponse { + List []SubscribeGroup `json:"list"` + Total int64 `json:"total"` + } + GetUserSubscribeTrafficLogsRequest { + Page int `form:"page"` + Size int `form:"size"` + UserId int64 `form:"user_id"` + SubscribeId int64 `form:"subscribe_id"` + StartTime int64 `form:"start_time"` + EndTime int64 `form:"end_time"` + } + GetUserSubscribeTrafficLogsResponse { + List []TrafficLog `json:"list"` + Total int64 `json:"total"` + } + QueryUserAffiliateListResponse { + List []UserAffiliate `json:"list"` + Total int64 `json:"total"` + } + PlatformResponse { + List []PlatformInfo `json:"list"` + } + PlatformInfo { + Platform string `json:"platform"` + PlatformUrl string `json:"platform_url"` + PlatformFieldDescription map[string]string `json:"platform_field_description"` + } + AlipayNotifyResponse { + ReturnCode string `json:"return_code"` + } + EPayNotifyRequest { + Pid int64 `json:"pid" form:"pid"` + TradeNo string `json:"trade_no" form:"trade_no"` + OutTradeNo string `json:"out_trade_no" form:"out_trade_no"` + Type string `json:"type" form:"type"` + Name string `json:"name" form:"name"` + Money string `json:"money" form:"money"` + TradeStatus string `json:"trade_status" form:"trade_status"` + Param string `json:"param" form:"param"` + Sign string `json:"sign" form:"sign"` + SignType string `json:"sign_type" form:"sign_type"` + } + QueryUserAffiliateListRequest { + Page int `form:"page"` + Size int `form:"size"` + } + CheckoutOrderRequest { + OrderNo string `json:"orderNo"` + ReturnUrl string `json:"returnUrl,omitempty"` + } + CheckoutOrderResponse { + Type string `json:"type"` + CheckoutUrl string `json:"checkout_url,omitempty"` + Stripe *StripePayment `json:"stripe,omitempty"` + } + SiteCustomDataContacts { + Email string `json:"email"` + Telephone string `json:"telephone"` + Address string `json:"address"` + } + + QueryUserAffiliateCountResponse { + Registers int64 `json:"registers"` + TotalCommission int64 `json:"total_commission"` + } + + AppUserSubcbribe{ + Id int64 `json:"id"` + Name string `json:"name"` + Upload int64 `json:"upload"` + Traffic int64 `json:"traffic"` + Download int64 `json:"download"` + DeviceLimit int64 `json:"device_limit"` + StartTime string `json:"start_time"` + ExpireTime string `json:"expire_time"` + List []AppUserSubscbribeNode `json:"list"` + } + + AppUserSubscbribeNode{ + Id int64 `json:"id"` + Name string `json:"name"` + Uuid string `json:"uuid"` + Protocol string `json:"protocol"` + RelayMode string `json:"relay_mode"` + RelayNode string `json:"relay_node"` + ServerAddr string `json:"server_addr"` + SpeedLimit int `json:"speed_limit"` + Tags []string `json:"tags"` + Traffic int64 `json:"traffic"` + TrafficRatio float64 `json:"traffic_ratio"` + Upload int64 `json:"upload"` + Config string `json:"config"` + Country string `json:"country"` + City string `json:"city"` + Latitude string `json:"latitude"` + Longitude string `json:"longitude"` + LatitudeCountry string `json:"latitudeCountry"` + LongitudeCountry string `json:"longitudeCountry"` + CreatedAt int64 `json:"created_at"` + Download int64 `json:"download"` + } +) + diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..b9fd494 --- /dev/null +++ b/build.bat @@ -0,0 +1,4 @@ +SET CGO_ENABLED=0 +SET GOOS=linux +SET GOARCH=amd64 +go build -o ppanel .\ppanel.go \ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..4f46257 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(startCmd) + rootCmd.AddCommand(versionCmd) +} + +var rootCmd = &cobra.Command{ + Use: "PPanel", + Short: "PPanel is a modern multi-user agent panel.", + Long: `[ PPanel is a pure, professional, and perfect open-source proxy panel tool, designed to be your ideal choice for learning and practical use.] +[ Simple and easy to operate.]`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("args:", args) + }, +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 0000000..a96d3d8 --- /dev/null +++ b/cmd/run.go @@ -0,0 +1,155 @@ +package cmd + +import ( + "context" + "fmt" + + "log" + "os" + "os/signal" + "syscall" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + + "github.com/perfect-panel/ppanel-server/initialize" + "github.com/perfect-panel/ppanel-server/internal" + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/conf" + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/orm" + "github.com/perfect-panel/ppanel-server/pkg/service" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/queue" + "github.com/perfect-panel/ppanel-server/scheduler" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" +) + +func init() { + startCmd.Flags().StringVar(&startConfigPath, "config", "etc/ppanel.yaml", "ppanel.yaml directory to read from") +} + +var ( + startConfigPath string +) + +var startCmd = &cobra.Command{ + Use: "run", + Short: "start PPanel", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("[PPanel version] v" + constant.Version) + run() + }, +} + +func run() { + services := getServers() + defer services.Stop() + go services.Start() + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + <-quit +} +func getServers() *service.Group { + var c config.Config + + // check config file is exist + if _, err := os.Stat(startConfigPath); os.IsNotExist(err) { + // check directory is existed + if _, err := os.Stat("etc"); os.IsNotExist(err) { + logger.Errorf("Directory %s does not exist. Creating it...\n", "etc") + if err = os.MkdirAll("etc", os.ModePerm); err != nil { + log.Fatalf("Please create the directory %s and place the configuration file %s in it.\n", "etc", startConfigPath) + } + } + // create new config file + if _, err := os.Create(startConfigPath); err != nil { + logger.Errorf("Please create the configuration file %s in the directory %s.\n", startConfigPath, "etc") + panic(fmt.Sprintf("Please create the configuration file %s in the directory %s.\n", startConfigPath, "etc")) + } + } + // check config file is empty, if empty, start init web server + if initConfig(&c) { + status, server := initialize.Config(startConfigPath) + <-status + if err := server.Shutdown(context.TODO()); err != nil { + log.Printf("Init Server Shutdown: %s\n", err.Error()) + } + } + conf.MustLoad(startConfigPath, &c) + if !c.Debug { + gin.SetMode(gin.ReleaseMode) + } + // init logger + if err := logger.SetUp(c.Logger); err != nil { + logger.Errorf("Logger setup failed: %v", err.Error()) + } + + // init service context + ctx := svc.NewServiceContext(c) + services := service.NewServiceGroup() + services.Add(internal.NewService(ctx)) + services.Add(queue.NewService(ctx)) + services.Add(scheduler.NewService(ctx)) + return services +} + +func initConfig(c *config.Config) bool { + // load config + conf.MustLoad(startConfigPath, c) + // check custom config + if startConfigPath != "etc/ppanel.yaml" && c.MySQL.Addr == "" { + return true + } + // check access secret + if c.JwtAuth.AccessSecret == "" && startConfigPath == "etc/ppanel.yaml" { + c.JwtAuth.AccessSecret = uuid.New().String() + // Get environment variables + dsn := os.Getenv("PPANEL_DB") + if dsn == "" { + return true + } + cfg := orm.ParseDSN(dsn) + if cfg == nil { + return true + } else { + c.MySQL = *cfg + } + + // Get environment variables + uri := os.Getenv("PPANEL_REDIS") + if uri == "" { + return true + } + addr, pass, db, err := tool.ParseRedisURI(uri) + if err != nil { + return true + } else { + c.Redis.Host = addr + c.Redis.Pass = pass + c.Redis.DB = db + } + // save yaml file + newConfig := config.File{ + Host: c.Host, + Port: c.Port, + Debug: c.Debug, + JwtAuth: c.JwtAuth, + Logger: c.Logger, + MySQL: c.MySQL, + Redis: c.Redis, + } + fileData, err := yaml.Marshal(newConfig) + if err != nil { + panic(err.Error()) + } + // write to file + if err := os.WriteFile(startConfigPath, fileData, 0644); err != nil { + panic(err.Error()) + } + } + return false +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..804c976 --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,16 @@ +package cmd + +import ( + "fmt" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/spf13/cobra" +) + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "PPanel version", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("[PPanel version] " + config.Version) + }, +} diff --git a/doc/config-zh.md b/doc/config-zh.md new file mode 100644 index 0000000..1f2e409 --- /dev/null +++ b/doc/config-zh.md @@ -0,0 +1,88 @@ +### 配置文件说明 + +#### 1. 配置文件路径 + +配置文件默认路径为:`./etc/ppanel.yaml`,可通过启动参数 `--config` 指定配置文件路径。 + +#### 2. 配置文件格式 + - 配置文件为yaml格式,支持注释,命名为xxx.yaml。 + +```yaml +# 配置文件示例 +Host: # 服务监听地址,默认: 0.0.0.0 +Port: # 服务监听端口,默认: 8080 +Debug: # 是否开启调试模式,开启后无法使用后台日志功能, 默认: false +JwtAuth: # JWT认证配置 + AccessSecret: # 访问令牌密钥, 默认: 随机生成 + AccessExpire: # 访问令牌过期时间,单位秒, 默认: 604800 +Logger: # 日志配置 + FilePath: # 日志文件路径, 默认: ./ppanel.log + MaxSize: # 日志文件最大大小,单位MB, 默认: 50 + MaxBackup: # 日志文件最大备份数, 默认: 3 + MaxAge: # 日志文件最大保存时间,单位天, 默认: 30 + Compress: # 是否压缩日志文件, 默认: true + Level: # 日志级别, 默认: info, 可选: debug, info, warn, error, panic, panic, fatal +MySQL: + Addr: # MySQL地址, 必填 + Username: # MySQL用户名, 必填 + Password: # MySQL密码, 必填 + Dbname: # MySQL数据库名, 必填 + Config: # Mysql配置默认值 charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai + MaxIdleConns: # 最大空闲连接数, 默认: 10 + MaxOpenConns: # 最大打开连接数, 默认: 100 + LogMode: # 日志级别, 默认: info, 可选: debug, error, warn, info + LogZap: # 是否使用zap日志记录sql, 默认: true + SlowThreshold: # 慢查询阈值,单位毫秒, 默认: 1000 +Redis: + Host: # Redis地址, 默认:localhost:6379 + Pass: # Redis密码, 默认: "" + DB: # Redis数据库, 默认: 0 + +Administer: + Email: # 后台登录邮箱, 默认: admin@ppanel.dev + Password: # 后台登录密码, 默认: password + +``` + +#### 3. 配置文件说明 + +- `Host`: 服务监听地址,默认: **0.0.0.0** +- `Port`: 服务监听端口,默认: **8080** +- `Debug`: 是否开启调试模式,开启后无法使用后台日志功能, 默认: **false** +- `JwtAuth`: JWT认证配置 + - `AccessSecret`: 访问令牌密钥, 默认: **随机生成** + - `AccessExpire`: 访问令牌过期时间,单位秒, 默认: **604800** +- `Logger`: 日志配置 +- `FilePath`: 日志文件路径, 默认: **./ppanel.log** +- `MaxSize`: 日志文件最大大小,单位MB, 默认: **50** +- `MaxBackup`: 日志文件最大备份数, 默认: **3** +- `MaxAge`: 日志文件最大保存时间,单位天, 默认: **30** +- `Compress`: 是否压缩日志文件, 默认: **true** +- `Level`: 日志级别, 默认: **info**, 可选: **debug, info, warn, error, panic, panic, fatal** +- `MySQL`: MySQL配置 + - `Addr`: MySQL地址, 必填 + - `Username`: MySQL用户名, 必填 + - `Password`: MySQL密码, 必填 + - `Dbname`: MySQL数据库名, 必填 + - `Config`: Mysql配置默认值 charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai + - `MaxIdleConns`: 最大空闲连接数, 默认: **10** + - `MaxOpenConns`: 最大打开连接数, 默认: **100** + - `LogMode`: 日志级别, 默认: **info**, 可选: **debug, error, warn, info** + - `LogZap`: 是否使用zap日志记录sql, 默认: **true** + - `SlowThreshold`: 慢查询阈值,单位毫秒, 默认: **1000** +- `Redis`: Redis配置 +- `Host`: Redis地址, 默认: **localhost:6379** +- `Pass`: Redis密码, 默认: **""** +- `DB`: Redis数据库, 默认: **0** +- `Administer`: 后台登录配置 + - `Email`: 后台登录邮箱, 默认: **admin@ppanel.dev** + - `Password`: 后台登录密码, 默认: **password** + +#### 4. 环境变量 + +支持的环境变量如下: + +| 环境变量 | 配置项 | 示例 | +|--------------|---------|:-------------------------------------------| +| PPANEL_DB | MySQL配置 | root:password@tcp(localhost:3306)/vpnboard | +| PPANEL_REDIS | Redis配置 | redis://localhost:6379" | diff --git a/doc/config.md b/doc/config.md new file mode 100644 index 0000000..ebb52c1 --- /dev/null +++ b/doc/config.md @@ -0,0 +1,116 @@ +### Configuration File Instructions +#### Configuration File Path + +The default configuration file path is ./etc/ppanel.yaml. You can specify a custom path using the --config startup parameter. + +#### Configuration File Format + +The configuration file uses the YAML format, supports comments, and should be named xxx.yaml. + +```yaml +# Sample Configuration File +Host: # Service listening address, default: 0.0.0.0 +Port: # Service listening port, default: 8080 +Debug: # Enable debug mode; disables backend logging when enabled, default: false +JwtAuth: # JWT authentication settings + AccessSecret: # Access token secret, default: randomly generated + AccessExpire: # Access token expiration time in seconds, default: 604800 +Logger: # Logging configuration + FilePath: # Log file path, default: ./ppanel.log + MaxSize: # Maximum log file size in MB, default: 50 + MaxBackup: # Maximum number of log file backups, default: 3 + MaxAge: # Maximum log file retention time in days, default: 30 + Compress: # Whether to compress log files, default: true + Level: # Logging level, default: info; options: debug, info, warn, error, panic, fatal +MySQL: + Addr: # MySQL address, required + Username: # MySQL username, required + Password: # MySQL password, required + Dbname: # MySQL database name, required + Config: # MySQL configuration, default: charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai + MaxIdleConns: # Maximum idle connections, default: 10 + MaxOpenConns: # Maximum open connections, default: 100 + LogMode: # Log level, default: info; options: debug, error, warn, info + LogZap: # Whether to use zap for SQL logging, default: true + SlowThreshold: # Slow query threshold in milliseconds, default: 1000 +Redis: + Host: # Redis address, default: localhost:6379 + Pass: # Redis password, default: "" + DB: # Redis database, default: 0 + +Administer: + Email: # Admin login email, default: admin@ppanel.dev + Password: # Admin login password, default: password +``` + +#### 3.Configuration Descriptions + +- Host: Service listening address, default: 0.0.0.0 + +- Port: Service listening port, default: 8080 +- Debug: Enable debug mode; disables backend logging when enabled, default: false + +- JwtAuth: JWT authentication settings + + - AccessSecret: Access token secret, default: randomly generated + + - AccessExpire: Access token expiration time in seconds, default: 604800 + + - Logger: Logging configuration + + - FilePath: Log file path, default: ./ppanel.log + + - MaxSize: Maximum log file size in MB, default: 50 + + - MaxBackup: Maximum number of log file backups, default: 3 + + - MaxAge: Maximum log file retention time in days, default: 30 + + - Compress: Whether to compress log files, default: true + + - Level: Logging level, default: info; options: debug, info, warn, error, panic, fatal + +- MySQL: MySQL configuration + + - Addr: MySQL address, required + + - Username: MySQL username, required + + - Password: MySQL password, required + + - Dbname: MySQL database name, required + + - Config: MySQL configuration, default: charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai + + - MaxIdleConns: Maximum idle connections, default: 10 + + - MaxOpenConns: Maximum open connections, default: 100 + + - LogMode: Log level, default: info; options: debug, error, warn, info + + - LogZap: Whether to use zap for SQL logging, default: true + + - SlowThreshold: Slow query threshold in milliseconds, default: 1000 + +- Redis: Redis configuration + + - Host: Redis address, default: localhost:6379 + + - Pass: Redis password, default: "" + + - DB: Redis database, default: 0 + +- Administer: Admin login configuration + + - Email: Admin login email, default: admin@ppanel.dev + + - Password: Admin login password, default: password + +#### 4. Environment Variables + +Supported environment variables are as follows: + +| Environment Variable | Configuration | Example | +|-----------------------|---------------|:-------------------------------------------| +| PPANEL_DB | MySQL config | root:password@tcp(localhost:3306)/vpnboard | +| PPANEL_REDIS | Redis config | redis://localhost:6379" | diff --git a/doc/install-zh.md b/doc/install-zh.md new file mode 100644 index 0000000..7410580 --- /dev/null +++ b/doc/install-zh.md @@ -0,0 +1,133 @@ +### 安装说明 +#### 前置系统要求 +- Mysql 5.7+ (推荐使用8.0) +- Redis 6.0+ (推荐使用7.0) + +#### 二进制安装 +1.确定系统架构,并下载对应的二进制文件 + +下载地址:`https://github.com/perfect-panel/ppanel/releases` + +示例说明:系统:Linux amd64,用户:root,当前目录:/root + +- 下载二进制文件 + +```shell +$ wget https://github.com/perfect-panel/ppanel/releases/download/v0.1.0/ppanel-server-linux-amd64.tar.gz +``` + +- 解压二进制文件 + +```shell +$ tar -zxvf ppanel-server-linux-amd64.tar.gz +``` + +- 进入解压后的目录 + +```shell +$ cd ppanel-server-linux-amd64 +``` + +- 赋予二进制文件执行权限 + +```shell +$ chmod +x ppanel-server +``` + +- 创建 systemd 服务文件 + +```shell +$ cat > /etc/systemd/system/ppanel.service < /etc/systemd/system/ppanel.service <:8080/init` to **initialize the system configuration**. + +#### NGINX Reverse Proxy Configuration + +Below is an example configuration to proxy the ppanel service to the domain api.ppanel.dev: + +```nginx +server { + listen 80; + server_name ppanel.dev; + + location / { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header REMOTE-HOST $remote_addr; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_http_version 1.1; + + add_header X-Cache $upstream_cache_status; + + # Set Nginx Cache + set $static_file_cache 0; + if ($uri ~* "\.(gif|png|jpg|css|js|woff|woff2)$") { + set $static_file_cache 1; + expires 1m; + } + if ($static_file_cache = 0) { + add_header Cache-Control no-cache; + } + } +} +``` + +If using Cloudflare as a proxy service, you need to retrieve the user's real IP address. Add the following to the http section of the NGINX configuration file: + +- Dependency: `ngx_http_realip_module`. Check if your NGINX build includes this module by running `nginx -V`. If not, you will need to recompile NGINX with this module. + +```nginx +# Cloudflare Start +set_real_ip_from 0.0.0.0/0; +real_ip_header X-Forwarded-For; +real_ip_recursive on; +# Cloudflare End +``` \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c2b3225 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3' + +services: + ppanel: + container_name: ppanel-server + build: + context: . + dockerfile: Dockerfile + ports: + - "8080:8080" + volumes: + - ./etc/ppanel.yaml:/app/etc/ppanel.yaml + restart: always diff --git a/generate/gopure-amd64.exe b/generate/gopure-amd64.exe new file mode 100644 index 0000000..cd250fb Binary files /dev/null and b/generate/gopure-amd64.exe differ diff --git a/generate/gopure-arm64.exe b/generate/gopure-arm64.exe new file mode 100644 index 0000000..3b90adb Binary files /dev/null and b/generate/gopure-arm64.exe differ diff --git a/generate/gopure-darwin-amd64 b/generate/gopure-darwin-amd64 new file mode 100644 index 0000000..496dd4b Binary files /dev/null and b/generate/gopure-darwin-amd64 differ diff --git a/generate/gopure-darwin-arm64 b/generate/gopure-darwin-arm64 new file mode 100644 index 0000000..4c7f6b8 Binary files /dev/null and b/generate/gopure-darwin-arm64 differ diff --git a/generate/gopure-linux-amd64 b/generate/gopure-linux-amd64 new file mode 100644 index 0000000..80832ef Binary files /dev/null and b/generate/gopure-linux-amd64 differ diff --git a/generate/gopure-linux-arm64 b/generate/gopure-linux-arm64 new file mode 100644 index 0000000..ee5d21d Binary files /dev/null and b/generate/gopure-linux-arm64 differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5f32d38 --- /dev/null +++ b/go.mod @@ -0,0 +1,135 @@ +module github.com/perfect-panel/ppanel-server + +go 1.23.3 + +require ( + github.com/GUAIK-ORG/go-snowflake v0.0.0-20200116064823-220c4260e85f + github.com/alibabacloud-go/darabonba-openapi v0.1.18 + github.com/alibabacloud-go/dysmsapi-20170525/v2 v2.0.18 + github.com/alibabacloud-go/tea v1.2.2 + github.com/alicebob/miniredis/v2 v2.34.0 + github.com/anaskhan96/go-password-encoder v0.0.0-20201010210601-c765b799fd72 + github.com/andybalholm/brotli v1.1.1 + github.com/forgoer/openssl v1.6.0 + github.com/gin-contrib/sessions v1.0.1 + github.com/gin-gonic/gin v1.10.0 + github.com/go-playground/locales v0.14.1 + github.com/go-playground/universal-translator v0.18.1 + github.com/go-playground/validator/v10 v10.24.0 + github.com/go-resty/resty/v2 v2.15.3 + github.com/go-sql-driver/mysql v1.8.1 + github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 + github.com/gofrs/uuid/v5 v5.3.0 + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.3 + github.com/hibiken/asynq v0.24.1 + github.com/jinzhu/copier v0.4.0 + github.com/klauspost/compress v1.17.7 + github.com/nyaruka/phonenumbers v1.5.0 + github.com/pkg/errors v0.9.1 + github.com/redis/go-redis/v9 v9.6.1 + github.com/smartwalle/alipay/v3 v3.2.23 + github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/cobra v1.8.1 + github.com/stretchr/testify v1.10.0 + github.com/stripe/stripe-go/v81 v81.1.0 + github.com/twilio/twilio-go v1.23.11 + go.opentelemetry.io/otel v1.24.0 + go.opentelemetry.io/otel/exporters/jaeger v1.17.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 + go.opentelemetry.io/otel/exporters/zipkin v1.24.0 + go.opentelemetry.io/otel/sdk v1.24.0 + go.opentelemetry.io/otel/trace v1.24.0 + go.uber.org/zap v1.27.0 + golang.org/x/crypto v0.32.0 + golang.org/x/oauth2 v0.25.0 + golang.org/x/time v0.6.0 + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df + gopkg.in/yaml.v3 v3.0.1 + gorm.io/driver/mysql v1.5.7 + gorm.io/gorm v1.25.12 + gorm.io/plugin/soft_delete v1.2.1 + k8s.io/apimachinery v0.31.1 +) + +require ( + github.com/fatih/color v1.18.0 + github.com/goccy/go-json v0.10.4 + github.com/spaolacci/murmur3 v1.1.0 + google.golang.org/grpc v1.61.1 + google.golang.org/protobuf v1.36.3 +) + +require ( + cloud.google.com/go/compute/metadata v0.6.0 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect + github.com/alibabacloud-go/debug v1.0.1 // indirect + github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect + github.com/alibabacloud-go/openapi-util v0.1.1 // indirect + github.com/alibabacloud-go/tea-utils v1.4.5 // indirect + github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect + github.com/alibabacloud-go/tea-xml v1.1.3 // indirect + github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect + github.com/aliyun/credentials-go v1.3.10 // indirect + github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect + github.com/bytedance/sonic v1.12.7 // indirect + github.com/bytedance/sonic/loader v0.2.3 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clbanning/mxj/v2 v2.5.6 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.0.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/glog v1.1.2 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/gomodule/redigo v2.0.0+incompatible // indirect + github.com/gorilla/context v1.1.2 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/gorilla/sessions v1.2.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/openzipkin/zipkin-go v0.4.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/smartwalle/ncrypto v1.0.4 // indirect + github.com/smartwalle/ngx v1.0.9 // indirect + github.com/smartwalle/nsign v1.0.9 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/tjfoc/gmsm v1.4.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/yuin/gopher-lua v1.1.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/proto/otlp v1.1.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/arch v0.13.0 // indirect + golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/ini.v1 v1.67.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..841511b --- /dev/null +++ b/go.sum @@ -0,0 +1,505 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/GUAIK-ORG/go-snowflake v0.0.0-20200116064823-220c4260e85f h1:RDkg3pyE1qGbBpRWmvSN9RNZC5nUrOaEPiEpEb8y2f0= +github.com/GUAIK-ORG/go-snowflake v0.0.0-20200116064823-220c4260e85f/go.mod h1:zA7AF9RTfpluCfz0omI4t5KCMaWHUMicsZoMccnaT44= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g= +github.com/alibabacloud-go/darabonba-openapi v0.1.18 h1:3eUVmAr7WCJp7fgIvmCd9ZUyuwtJYbtUqJIed5eXCmk= +github.com/alibabacloud-go/darabonba-openapi v0.1.18/go.mod h1:PB4HffMhJVmAgNKNq3wYbTUlFvPgxJpTzd1F5pTuUsc= +github.com/alibabacloud-go/darabonba-string v1.0.0/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA= +github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY= +github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc= +github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg= +github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc= +github.com/alibabacloud-go/dysmsapi-20170525/v2 v2.0.18 h1:hfZA4cgIl6frNdsRmAyj8sn9J1bihQpYbzIVv2T/+Cs= +github.com/alibabacloud-go/dysmsapi-20170525/v2 v2.0.18/go.mod h1:di54xjBFHvKiQQo7st3TUmiMy0ywne5TOHup786Rhes= +github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q= +github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE= +github.com/alibabacloud-go/openapi-util v0.0.11/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= +github.com/alibabacloud-go/openapi-util v0.1.1 h1:ujGErJjG8ncRW6XtBBMphzHTvCxn4DjrVw4m04HsS28= +github.com/alibabacloud-go/openapi-util v0.1.1/go.mod h1:/UehBSE2cf1gYT43GV4E+RxTdLRzURImCYY0aRmlXpw= +github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg= +github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= +github.com/alibabacloud-go/tea v1.2.2 h1:aTsR6Rl3ANWPfqeQugPglfurloyBJY85eFy7Gc1+8oU= +github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk= +github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= +github.com/alibabacloud-go/tea-utils v1.4.3/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw= +github.com/alibabacloud-go/tea-utils v1.4.5 h1:h0/6Xd2f3bPE4XHTvkpjwxowIwRCJAJOqY6Eq8f3zfA= +github.com/alibabacloud-go/tea-utils v1.4.5/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw= +github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= +github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0= +github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= +github.com/alibabacloud-go/tea-xml v1.1.2/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8= +github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0= +github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8= +github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= +github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= +github.com/alicebob/miniredis/v2 v2.34.0 h1:mBFWMaJSNL9RwdGRyEDoAAv8OQc5UlEhLDQggTglU/0= +github.com/alicebob/miniredis/v2 v2.34.0/go.mod h1:kWShP4b58T1CW0Y5dViCd5ztzrDqRWqM3nksiyXk5s8= +github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw= +github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM= +github.com/aliyun/credentials-go v1.3.10 h1:45Xxrae/evfzQL9V10zL3xX31eqgLWEaIdCoPipOEQA= +github.com/aliyun/credentials-go v1.3.10/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= +github.com/anaskhan96/go-password-encoder v0.0.0-20201010210601-c765b799fd72 h1:a93gW7OBt55SksMQVibqPWdu4Ly73KM4d3zoIUUX3cs= +github.com/anaskhan96/go-password-encoder v0.0.0-20201010210601-c765b799fd72/go.mod h1:PsJICrlruG9QcJDYuZ0dO/2KtMDALzRbony8NkxZ2nE= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= +github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff h1:RmdPFa+slIr4SCBg4st/l/vZWVe9QJKMXGO60Bxbe04= +github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw= +github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q= +github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= +github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clbanning/mxj/v2 v2.5.6 h1:Jm4VaCI/+Ug5Q57IzEoZbwx4iQFA6wkXv72juUSeK+g= +github.com/clbanning/mxj/v2 v2.5.6/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/forgoer/openssl v1.6.0 h1:IueL+UfH0hKo99xFPojHLlO3QzRBQqFY+Cht0WwtOC0= +github.com/forgoer/openssl v1.6.0/go.mod h1:9DZ4yOsQmveP0aXC/BpQ++Y5TKaz5yR9+emcxmIZNZs= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/sessions v1.0.1 h1:3hsJyNs7v7N8OtelFmYXFrulAf6zSR7nW/putcPEHxI= +github.com/gin-contrib/sessions v1.0.1/go.mod h1:ouxSFM24/OgIud5MJYQJLpy6AwxQ5EYO9yLhbtObGkM= +github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= +github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg= +github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= +github.com/go-resty/resty/v2 v2.15.3 h1:bqff+hcqAflpiF591hhJzNdkRsFhlB96CYfBwSFvql8= +github.com/go-resty/resty/v2 v2.15.3/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= +github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= +github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk= +github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= +github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= +github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= +github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= +github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= +github.com/hibiken/asynq v0.24.1 h1:+5iIEAyA9K/lcSPvx3qoPtsKJeKI5u9aOIvUmSsazEw= +github.com/hibiken/asynq v0.24.1/go.mod h1:u5qVeSbrnfT+vtG5Mq8ZPzQu/BmCKMHvTGb91uy9Tts= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= +github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275 h1:IZycmTpoUtQK3PD60UYBwjaCUHUP7cML494ao9/O8+Q= +github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275/go.mod h1:zt6UU74K6Z6oMOYJbJzYpYucqdcQwSMPBEdSvGiaUMw= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nyaruka/phonenumbers v1.5.0 h1:0M+Gd9zl53QC4Nl5z1Yj1O/zPk2XXBUwR/vlzdXSJv4= +github.com/nyaruka/phonenumbers v1.5.0/go.mod h1:gv+CtldaFz+G3vHHnasBSirAi3O2XLqZzVWz4V1pl2E= +github.com/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA= +github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= +github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= +github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/smartwalle/alipay/v3 v3.2.23 h1:i1VwJeu70EmwpsXXz6GZZnMAtRx5MTfn2dPoql/L3zE= +github.com/smartwalle/alipay/v3 v3.2.23/go.mod h1:lVqFiupPf8YsAXaq5JXcwqnOUC2MCF+2/5vub+RlagE= +github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8= +github.com/smartwalle/ncrypto v1.0.4/go.mod h1:Dwlp6sfeNaPMnOxMNayMTacvC5JGEVln3CVdiVDgbBk= +github.com/smartwalle/ngx v1.0.9 h1:pUXDvWRZJIHVrCKA1uZ15YwNti+5P4GuJGbpJ4WvpMw= +github.com/smartwalle/ngx v1.0.9/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0= +github.com/smartwalle/nsign v1.0.9 h1:8poAgG7zBd8HkZy9RQDwasC6XZvJpDGQWSjzL2FZL6E= +github.com/smartwalle/nsign v1.0.9/go.mod h1:eY6I4CJlyNdVMP+t6z1H6Jpd4m5/V+8xi44ufSTxXgc= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stripe/stripe-go/v81 v81.1.0 h1:OlpGPO2vhS2raLR/NuvHKeRUZ57FTkdZBTcd5Hhoyos= +github.com/stripe/stripe-go/v81 v81.1.0/go.mod h1:C/F4jlmnGNacvYtBp/LUHCvVUJEZffFQCobkzwY1WOo= +github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= +github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= +github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= +github.com/twilio/twilio-go v1.23.11 h1:Q532m0rgWF1AzzF4Z4ejzTk5XeORWT+zLGzlklSk/iU= +github.com/twilio/twilio-go v1.23.11/go.mod h1:zRkMjudW7v7MqQ3cWNZmSoZJ7EBjPZ4OpNh2zm7Q6ko= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDOpt2M8OTG92cWqUESvzh2MxiR5xY8= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA= +go.opentelemetry.io/otel/exporters/zipkin v1.24.0 h1:3evrL5poBuh1KF51D9gO/S+N/1msnm4DaBqs/rpXUqY= +go.opentelemetry.io/otel/exporters/zipkin v1.24.0/go.mod h1:0EHgD8R0+8yRhUYJOGR8Hfg2dpiJQxDOszd5smVO9wM= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= +go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= +go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA= +golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4= +golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= +golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos= +google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0vFTBPy+tFSGvXEd3z+BcoG1k7EHbqm+YBsY= +google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 h1:rcS6EyEaoCO52hQDupoSfrxI3R6C2Tq741is7X8OvnM= +google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917/go.mod h1:CmlNWB9lSezaYELKS5Ym1r44VrrbPUa7JTvw+6MbpJ0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY= +google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= +gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c= +gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc= +gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= +gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.23.0/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +gorm.io/plugin/soft_delete v1.2.1 h1:qx9D/c4Xu6w5KT8LviX8DgLcB9hkKl6JC9f44Tj7cGU= +gorm.io/plugin/soft_delete v1.2.1/go.mod h1:Zv7vQctOJTGOsJ/bWgrN1n3od0GBAZgnLjEx+cApLGk= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= +k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/initialize/config.go b/initialize/config.go new file mode 100644 index 0000000..1df7ec1 --- /dev/null +++ b/initialize/config.go @@ -0,0 +1,277 @@ +package initialize + +import ( + "database/sql" + "embed" + "fmt" + "html/template" + "log" + "net/http" + "os" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + "gorm.io/driver/mysql" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/perfect-panel/ppanel-server/initialize/migrate" + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/pkg/conf" + "github.com/perfect-panel/ppanel-server/pkg/orm" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/pkg/errors" + "gopkg.in/yaml.v3" + "gorm.io/gorm" +) + +//go:embed templates/*.html +var templateFS embed.FS + +var initStatus = make(chan bool) +var configPath string + +func Config(path string) (chan bool, *http.Server) { + // Set the configuration file path + configPath = path + // Create a new Gin instance + r := gin.Default() + + // Create a new HTTP server + server := &http.Server{ + Addr: ":8080", + Handler: r, + } + // Load templates + tmpl := template.Must(template.ParseFS(templateFS, "templates/*.html")) + r.SetHTMLTemplate(tmpl) + + r.GET("/init", handleInit) + r.POST("/init/config", handleInitConfig) + r.POST("/init/mysql/test", HandleMySQLTest) + r.POST("/init/redis/test", HandleRedisTest) + // Handle 404 + r.NoRoute(func(c *gin.Context) { + c.Redirect(http.StatusFound, "/init") + }) + + go func(server *http.Server) { + // Start the server + if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("listen: %s\n", err) + } + }(server) + + return initStatus, server +} + +func handleInit(c *gin.Context) { + c.HTML(http.StatusOK, "index.html", nil) +} +func handleInitConfig(c *gin.Context) { + // Load configuration file + + var cfg config.File + conf.MustLoad(configPath, &cfg) + var request struct { + AdminEmail string `json:"adminEmail"` + AdminPassword string `json:"adminPassword"` + + MysqlHost string `json:"mysqlHost"` + MysqlPort string `json:"mysqlPort"` + MysqlDatabase string `json:"mysqlDatabase"` + MysqlUser string `json:"mysqlUser"` + MysqlPassword string `json:"mysqlPassword"` + + RedisHost string `json:"redisHost"` + RedisPort string `json:"redisPort"` + RedisPassword string `json:"redisPassword"` + } + if err := c.ShouldBindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "msg": "Invalid request", + "data": nil, + }) + c.Abort() + return + } + cfg.Debug = false + // jwt secret + cfg.JwtAuth.AccessSecret = uuid.New().String() + // mysql + cfg.MySQL.Addr = fmt.Sprintf("%s:%s", request.MysqlHost, request.MysqlPort) + cfg.MySQL.Dbname = request.MysqlDatabase + cfg.MySQL.Username = request.MysqlUser + cfg.MySQL.Password = request.MysqlPassword + // redis + cfg.Redis.Host = fmt.Sprintf("%s:%s", request.RedisHost, request.RedisPort) + cfg.Redis.Pass = request.RedisPassword + + // save config + fileData, err := yaml.Marshal(cfg) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": "Configuration initialization failed", + "data": nil, + }) + c.Abort() + return + } + + // create mysql connection + db, err := orm.ConnectMysql(orm.Mysql{ + Config: orm.Config{ + Addr: fmt.Sprintf("%s:%s", request.MysqlHost, request.MysqlPort), + Username: request.MysqlUser, + Password: request.MysqlPassword, + Dbname: request.MysqlDatabase, + Config: "charset%3Dutf8mb4%26parseTime%3Dtrue%26loc%3DLocal", + MaxIdleConns: 10, + MaxOpenConns: 10, + SlowThreshold: 1000, + }, + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": "MySQL connection failed", + "data": nil, + }) + c.Abort() + return + } + + // init + if err := initMysql(db, request.AdminEmail, request.AdminPassword); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": "MySQL initialization failed", + "data": nil, + }) + c.Abort() + return + } + + // write to file + if err := os.WriteFile(configPath, fileData, 0644); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": "Configuration initialization failed", + "data": nil, + }) + c.Abort() + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "msg": "Configuration initialized", + "status": true, + }) + initStatus <- true +} + +func HandleMySQLTest(c *gin.Context) { + var request struct { + Host string `json:"host"` + Port string `json:"port"` + Database string `json:"database"` + User string `json:"user"` + Password string `json:"password"` + } + if err := c.ShouldBindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "msg": "Invalid request", + "data": nil, + }) + c.Abort() + return + } + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", request.User, request.Password, request.Host, request.Port, request.Database) + var status = true + var message string + var tx *sql.DB + var tables []string + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) + if err != nil { + logger.Errorf("connect mysql failed, err: %v\n", err.Error()) + status = false + message = "MySQL connection failed" + goto result + } + tx, _ = db.DB() + if err := tx.Ping(); err != nil { + logger.Errorf("ping mysql failed, err: %v\n", err.Error()) + status = false + message = "MySQL connection failed" + } + + tables, err = db.Migrator().GetTables() + if err != nil { + logger.Errorf("database table check failed, err: %v\n", err.Error()) + status = false + message = "Database table check failed" + goto result + } + if len(tables) > 0 { + status = false + message = "The database contains existing data. Please clear it before proceeding with the installation." + goto result + } + +result: + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "msg": message, + "status": status, + }) +} + +func HandleRedisTest(c *gin.Context) { + var request struct { + Host string `json:"host"` + Port string `json:"port"` + Password string `json:"password"` + } + if err := c.ShouldBindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "msg": "Invalid request", + "data": nil, + }) + c.Abort() + return + } + if err := tool.RedisPing(fmt.Sprintf("%s:%s", request.Host, request.Port), request.Password, 0); err != nil { + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "msg": nil, + "status": false, + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "msg": nil, + "status": true, + }) +} + +func initMysql(tx *gorm.DB, email, password string) error { + tables, err := tx.Migrator().GetTables() + if err != nil { + return fmt.Errorf("database table validation failed: %w", err) + } + if len(tables) > 0 { + return errors.New("the database contains existing data. Please clear it before proceeding with the installation") + } + if err := migrate.InitPPanelSQL(tx); err != nil { + return fmt.Errorf("failed to initialize database: %w", err) + } + if err := migrate.CreateAdminUser(email, password, tx); err != nil { + return fmt.Errorf("failed to create admin user: %w", err) + } + return nil +} diff --git a/initialize/email.go b/initialize/email.go new file mode 100644 index 0000000..b740cca --- /dev/null +++ b/initialize/email.go @@ -0,0 +1,33 @@ +package initialize + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/model/auth" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/tool" +) + +// Email get email smtp config +func Email(ctx *svc.ServiceContext) { + logger.Debug("Email config initialization") + method, err := ctx.AuthModel.FindOneByMethod(context.Background(), "email") + if err != nil { + panic(fmt.Sprintf("failed to find email auth method: %v", err.Error())) + } + var cfg config.EmailConfig + var emailConfig = new(auth.EmailAuthConfig) + if err := emailConfig.Unmarshal(method.Config); err != nil { + panic(fmt.Sprintf("failed to unmarshal email auth config: %v", err.Error())) + } + tool.DeepCopy(&cfg, emailConfig) + cfg.Enable = *method.Enabled + value, _ := json.Marshal(emailConfig.PlatformConfig) + cfg.PlatformConfig = string(value) + ctx.Config.Email = cfg +} diff --git a/initialize/init.go b/initialize/init.go new file mode 100644 index 0000000..00453d5 --- /dev/null +++ b/initialize/init.go @@ -0,0 +1,22 @@ +package initialize + +import "github.com/perfect-panel/ppanel-server/internal/svc" + +func StartInitSystemConfig(svc *svc.ServiceContext) { + // Initialize the system configuration + Mysql(svc) + VerifyVersion(svc) + Site(svc) + Node(svc) + Email(svc) + Invite(svc) + Verify(svc) + Subscribe(svc) + Register(svc) + Mobile(svc) + TrafficDataToRedis(svc) + if !svc.Config.Debug { + Telegram(svc) + } + +} diff --git a/initialize/invite.go b/initialize/invite.go new file mode 100644 index 0000000..c696097 --- /dev/null +++ b/initialize/invite.go @@ -0,0 +1,23 @@ +package initialize + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" +) + +func Invite(ctx *svc.ServiceContext) { + // Initialize the system configuration + logger.Debug("Register config initialization") + configs, err := ctx.SystemModel.GetInviteConfig(context.Background()) + if err != nil { + logger.Error("[Init Invite Config] Get Invite Config Error: ", logger.Field("error", err.Error())) + return + } + var inviteConfig config.InviteConfig + tool.SystemConfigSliceReflectToStruct(configs, &inviteConfig) + ctx.Config.Invite = inviteConfig +} diff --git a/initialize/migrate/database/01200-patch.sql b/initialize/migrate/database/01200-patch.sql new file mode 100644 index 0000000..7c2c000 --- /dev/null +++ b/initialize/migrate/database/01200-patch.sql @@ -0,0 +1,54 @@ +-- 先检查 `email` 列是否存在,再删除 +SELECT COUNT(*) INTO @col_exists FROM information_schema.columns +WHERE table_schema = DATABASE() AND table_name = 'user' AND column_name = 'email'; + +SET @sql = IF(@col_exists > 0, 'ALTER TABLE `user` DROP COLUMN `email`', 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 先检查 `telephone` 列是否存在,再删除 +SELECT COUNT(*) INTO @col_exists FROM information_schema.columns +WHERE table_schema = DATABASE() AND table_name = 'user' AND column_name = 'telephone'; + +SET @sql = IF(@col_exists > 0, 'ALTER TABLE `user` DROP COLUMN `telephone`', 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 先检查 `telephone_area_code` 列是否存在,再删除 +SELECT COUNT(*) INTO @col_exists FROM information_schema.columns +WHERE table_schema = DATABASE() AND table_name = 'user' AND column_name = 'telephone_area_code'; + +SET @sql = IF(@col_exists > 0, 'ALTER TABLE `user` DROP COLUMN `telephone_area_code`', 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + + +-- 先检查 `idx_email` 索引是否存在,再删除 +SELECT COUNT(*) INTO @idx_exists FROM information_schema.statistics +WHERE table_schema = DATABASE() AND table_name = 'user' AND index_name = 'idx_email'; + +SET @sql = IF(@idx_exists > 0, 'ALTER TABLE `user` DROP INDEX `idx_email`', 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 先检查 `idx_telephone` 索引是否存在,再删除 +SELECT COUNT(*) INTO @idx_exists FROM information_schema.statistics +WHERE table_schema = DATABASE() AND table_name = 'user' AND index_name = 'idx_telephone'; + +SET @sql = IF(@idx_exists > 0, 'ALTER TABLE `user` DROP INDEX `idx_telephone`', 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 先检查 `idx_telephone_area_code` 索引是否存在,再删除 +SELECT COUNT(*) INTO @idx_exists FROM information_schema.statistics +WHERE table_schema = DATABASE() AND table_name = 'user' AND index_name = 'idx_telephone_area_code'; + +SET @sql = IF(@idx_exists > 0, 'ALTER TABLE `user` DROP INDEX `idx_telephone_area_code`', 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/initialize/migrate/database/01201-patch.sql b/initialize/migrate/database/01201-patch.sql new file mode 100644 index 0000000..d2f4364 --- /dev/null +++ b/initialize/migrate/database/01201-patch.sql @@ -0,0 +1,118 @@ +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- 检查表是否存在,如果存在则跳过创建 +CREATE TABLE IF NOT EXISTS `oauth_config` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `platform` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'platform', + `config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'OAuth Configuration', + `redirect` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Redirect URL', + `enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Enabled', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + UNIQUE KEY `uni_oauth_config_platform` (`platform`) + ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- 插入记录时忽略重复记录 +BEGIN; +INSERT IGNORE INTO `oauth_config` (`id`, `platform`, `config`, `redirect`, `enabled`, `created_at`, `updated_at`) VALUES +(1, 'apple', '{\"team_id\":\"\",\"key_id\":\"\",\"client_id\":\"\",\"client_secret\":\"\"}', '', 0, '2025-01-26 20:11:15.292', '2025-01-26 20:11:15.292'), +(2, 'google', '{\"client_id\":\"\",\"client_secret\":\"\"}', '', 0, '2025-01-26 20:11:15.292', '2025-01-26 20:11:15.292'), +(3, 'github', '{\"client_id\":\"\",\"client_secret\":\"\"}', '', 0, '2025-01-26 20:11:15.292', '2025-01-26 20:11:15.292'), +(4, 'facebook', '{\"client_id\":\"\",\"client_secret\":\"\"}', '', 0, '2025-01-26 20:11:15.292', '2025-01-26 20:11:15.292'), +(5, 'telegram', '{\"bot\":\"\",\"bot_token\":\"\"}', '', 0, '2025-01-26 20:11:15.292', '2025-01-26 20:11:15.292'); +COMMIT; + +-- 检测更新设置表 +BEGIN; +INSERT IGNORE INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`) VALUES +('sms', 'SmsEnabled', 'false', 'bool', '是否启用短信功能', NOW(), NOW()), +('sms', 'SmsKey', 'your-key', 'string', '短信服务用户名或Key',NOW(), NOW()), +('sms', 'SmsSecret', 'your-secret', 'string', '短信服务密码或Secret', NOW(), NOW()), +('sms', 'SmsSign', 'your-sign', 'string', '短信签名', NOW(), NOW()), +('sms', 'SmsTemplate', 'your-template', 'string', '短信模板ID', NOW(), NOW()), +('sms', 'SmsRegion', 'cn-hangzhou', 'string', '短信服务所在区域(适用于阿里云)', NOW(), NOW()), +('sms', 'SmsTemplate', '您的验证码是{{.Code}},请在5分钟内使用。', 'string', '自定义短信模板', NOW(), NOW()), +('sms', 'SmsTemplateCode', 'SMS_12345678', 'string', '阿里云国内短信模板代码',NOW(),NOW()), +('sms', 'SmsTemplateParam', '{\"code\":{{.Code}}}', 'string', '短信模板参数', NOW(), NOW()), +('sms', 'SmsPlatform', 'smsbao', 'string', '当前使用的短信平台', NOW(), NOW()), +('sms', 'SmsLimit', '10', 'int64', '可以发送的短信最大数量', NOW(), NOW()), +('sms', 'SmsInterval', '60', 'int64', '发送短信的时间间隔(单位:秒)',NOW(), NOW()), +('sms', 'SmsExpireTime', '300', 'int64', '短信验证码的过期时间(单位:秒)',NOW(), NOW()), +('email', 'EmailEnabled', 'true', 'bool', '启用邮箱登陆',NOW(), NOW()), +('email', 'EmailSmtpHost', '', 'string', '邮箱服务器地址', NOW(), NOW()), +('email', 'EmailSmtpPort', '465', 'int', '邮箱服务器端口',NOW(), NOW()), +('email', 'EmailSmtpUser', 'domain@f1shyu.com', 'string', '邮箱服务器用户名', NOW(), NOW()), +('email', 'EmailSmtpPass', 'password', 'string', '邮箱服务器密码', NOW(), NOW()), +('email', 'EmailSmtpFrom', 'domain@f1shyu.com', 'string', '发送邮件的邮箱',NOW(), NOW()), +('email', 'EmailSmtpSSL', 'true', 'bool', '邮箱服务器加密方式',NOW(), NOW()), +('email', 'EmailTemplate', '%s', 'string', '邮件模板',NOW(), NOW()), +('email', 'VerifyEmailTemplate', '', 'string', 'Verify Email template',NOW(), NOW()), +('email', 'MaintenanceEmailTemplate', '', 'string', 'Maintenance Email template',NOW(), NOW()), +('email', 'ExpirationEmailTemplate', '', 'string', 'Expiration Email template', NOW(), NOW()), +('email', 'EmailEnableVerify', 'true', 'bool', '是否开启邮箱验证', NOW(), NOW()), +('email', 'EmailEnableDomainSuffix', 'false', 'bool', '是否开启邮箱域名后缀限制',NOW(), NOW()), +('email', 'EmailDomainSuffixList', 'qq.com', 'string', '邮箱域名后缀列表',NOW(), NOW()); +COMMIT; + +-- User Device +CREATE TABLE IF NOT EXISTS `user_device` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `device_number` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device Number.', + `online` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Online', + `enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'EnableDeviceNumber', + `last_online` datetime(3) DEFAULT NULL COMMENT 'Last Online', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + CONSTRAINT `fk_user_user_devices` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- Mobile +CREATE TABLE IF NOT EXISTS `sms` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `content` text COLLATE utf8mb4_general_ci, + `platform` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL, + `area_code` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL, + `telephone` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL, + `status` tinyint(1) DEFAULT '1', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- Application Config +CREATE TABLE IF NOT EXISTS `application_config` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `app_id` bigint NOT NULL DEFAULT '0' COMMENT 'App id', + `encryption_key` text COLLATE utf8mb4_general_ci COMMENT 'Encryption Key', + `encryption_method` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Encryption Method', + `domains` text COLLATE utf8mb4_general_ci, + `startup_picture` text COLLATE utf8mb4_general_ci, + `startup_picture_skip_time` bigint NOT NULL DEFAULT '0' COMMENT 'Startup Picture Skip Time', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- Application Version +CREATE TABLE IF NOT EXISTS `application_version` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `url` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用地址', + `version` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用版本', + `platform` varchar(50) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用平台', + `is_default` tinyint(1) NOT NULL DEFAULT '0' COMMENT '默认版本', + `description` text COLLATE utf8mb4_general_ci COMMENT '更新描述', + `application_id` bigint DEFAULT NULL COMMENT '所属应用', + `created_at` datetime(3) DEFAULT NULL COMMENT '创建时间', + `updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `fk_application_application_versions` (`application_id`), + CONSTRAINT `fk_application_application_versions` FOREIGN KEY (`application_id`) REFERENCES `application` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +UPDATE `subscribe` SET `unit_time`='Month' WHERE unit_time = ''; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/initialize/migrate/database/01202-patch.sql b/initialize/migrate/database/01202-patch.sql new file mode 100644 index 0000000..95c3a14 --- /dev/null +++ b/initialize/migrate/database/01202-patch.sql @@ -0,0 +1,44 @@ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +DROP TABLE IF EXISTS `user_device`; +-- User Device +CREATE TABLE IF NOT EXISTS `user_device` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `subscribe_id` bigint DEFAULT NULL COMMENT 'Subscribe ID', + `ip` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device Ip.', + `Identifier` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device Identifier.', + `user_agent` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device User Agent.', + `online` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Online', + `enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'EnableDeviceNumber', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + CONSTRAINT `fk_user_user_devices` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- ---------------------------- +-- Table structure for server_rule_group +-- ---------------------------- +DROP TABLE IF EXISTS `server_rule_group`; +CREATE TABLE `server_rule_group` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Rule Group Name', + `icon` text COLLATE utf8mb4_general_ci COMMENT 'Rule Group Icon', + `description` varchar(255) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Rule Group Description', + `enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Rule Group Enable', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + UNIQUE KEY `unique_name` (`name`) -- Add unique constraint to `name` +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +-- ---------------------------- +-- Records of server_rule_group +-- ---------------------------- +BEGIN; +COMMIT; + +SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/initialize/migrate/database/ppanel.sql b/initialize/migrate/database/ppanel.sql new file mode 100644 index 0000000..f15cdf9 --- /dev/null +++ b/initialize/migrate/database/ppanel.sql @@ -0,0 +1,562 @@ +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for ads +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `ads` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `title` varchar(255) COLLATE utf8mb4_german2_ci NOT NULL DEFAULT '' COMMENT 'Ads title', + `type` varchar(255) COLLATE utf8mb4_german2_ci NOT NULL DEFAULT '' COMMENT 'Ads type', + `content` text COLLATE utf8mb4_german2_ci COMMENT 'Ads content', + `target_url` varchar(512) COLLATE utf8mb4_german2_ci DEFAULT '' COMMENT 'Ads target url', + `start_time` datetime DEFAULT NULL COMMENT 'Ads start time', + `end_time` datetime DEFAULT NULL COMMENT 'Ads end time', + `status` tinyint(1) DEFAULT '0' COMMENT 'Ads status,0 disable,1 enable', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci; + + +-- ---------------------------- +-- Table structure for announcement +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `announcement` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `title` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Title', + `content` text COLLATE utf8mb4_general_ci COMMENT 'Content', + `show` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Show', + `pinned` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Pinned', + `popup` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Popup', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +-- ---------------------------- +-- Table structure for application +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `application` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用名称', + `icon` text COLLATE utf8mb4_general_ci NOT NULL COMMENT '应用图标', + `description` text COLLATE utf8mb4_general_ci COMMENT '更新描述', + `subscribe_type` varchar(50) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '订阅类型', + `created_at` datetime(3) DEFAULT NULL COMMENT '创建时间', + `updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +-- ---------------------------- +-- Table structure for application_config +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `application_config` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `app_id` bigint NOT NULL DEFAULT '0' COMMENT 'App id', + `encryption_key` text COLLATE utf8mb4_general_ci COMMENT 'Encryption Key', + `encryption_method` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Encryption Method', + `domains` text COLLATE utf8mb4_general_ci, + `startup_picture` text COLLATE utf8mb4_general_ci, + `startup_picture_skip_time` bigint NOT NULL DEFAULT '0' COMMENT 'Startup Picture Skip Time', + `invitation_link` text COLLATE utf8mb4_general_ci COMMENT 'Invitation Link', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +-- ---------------------------- +-- Table structure for application_version +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `application_version` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `url` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用地址', + `version` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用版本', + `platform` varchar(50) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用平台', + `is_default` tinyint(1) NOT NULL DEFAULT '0' COMMENT '默认版本', + `description` text COLLATE utf8mb4_general_ci COMMENT '更新描述', + `application_id` bigint DEFAULT NULL COMMENT '所属应用', + `created_at` datetime(3) DEFAULT NULL COMMENT '创建时间', + `updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `fk_application_application_versions` (`application_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- ---------------------------- +-- Table structure for coupon +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `coupon` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Coupon Name', + `code` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Coupon Code', + `count` bigint NOT NULL DEFAULT '0' COMMENT 'Count Limit', + `type` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Coupon Type: 1: Percentage 2: Fixed Amount', + `discount` bigint NOT NULL DEFAULT '0' COMMENT 'Coupon Discount', + `start_time` bigint NOT NULL DEFAULT '0' COMMENT 'Start Time', + `expire_time` bigint NOT NULL DEFAULT '0' COMMENT 'Expire Time', + `user_limit` bigint NOT NULL DEFAULT '0' COMMENT 'User Limit', + `subscribe` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Subscribe Limit', + `used_count` bigint NOT NULL DEFAULT '0' COMMENT 'Used Count', + `enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Enable', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + UNIQUE KEY `uni_coupon_code` (`code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- ---------------------------- +-- Table structure for document +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `document` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `title` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Document Title', + `content` text COLLATE utf8mb4_general_ci COMMENT 'Document Content', + `tags` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Document Tags', + `show` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Show', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +-- ---------------------------- +-- Table structure for auth_method +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `auth_method` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `method` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'method', + `config` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'OAuth Configuration', + `enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Enabled', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + UNIQUE KEY `uni_auth_method` (`method`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- ---------------------------- +-- Table structure for order +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `order` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `parent_id` bigint DEFAULT NULL COMMENT 'Parent Order Id', + `user_id` bigint NOT NULL DEFAULT '0' COMMENT 'User Id', + `order_no` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Order No', + `type` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Order Type: 1: Subscribe, 2: Renewal, 3: ResetTraffic, 4: Recharge', + `quantity` bigint NOT NULL DEFAULT '1' COMMENT 'Quantity', + `price` bigint NOT NULL DEFAULT '0' COMMENT 'Original price', + `amount` bigint NOT NULL DEFAULT '0' COMMENT 'Order Amount', + `gift_amount` bigint NOT NULL DEFAULT '0' COMMENT 'User Gift Amount', + `discount` bigint NOT NULL DEFAULT '0' COMMENT 'Discount Amount', + `coupon` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Coupon', + `coupon_discount` bigint NOT NULL DEFAULT '0' COMMENT 'Coupon Discount Amount', + `commission` bigint NOT NULL DEFAULT '0' COMMENT 'Order Commission', + `payment_id` bigint NOT NULL DEFAULT '-1' COMMENT 'Payment Id', + `method` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Payment Method', + `fee_amount` bigint NOT NULL DEFAULT '0' COMMENT 'Fee Amount', + `trade_no` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Trade No', + `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Order Status: 1: Pending, 2: Paid, 3:Close, 4: Failed, 5:Finished', + `subscribe_id` bigint NOT NULL DEFAULT '0' COMMENT 'Subscribe Id', + `subscribe_token` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Renewal Subscribe Token', + `is_new` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is New Order', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + UNIQUE KEY `uni_order_order_no` (`order_no`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- ---------------------------- +-- Table structure for payment +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `payment` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Payment Name', + `platform` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Payment Platform', + `description` text COLLATE utf8mb4_general_ci COMMENT 'Payment Description', + `icon` varchar(255) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Payment Icon', + `domain` varchar(255) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Notification Domain', + `config` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Payment Configuration', + `fee_mode` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Fee Mode: 0: No Fee 1: Percentage 2: Fixed Amount 3: Percentage + Fixed Amount', + `fee_percent` bigint DEFAULT '0' COMMENT 'Fee Percentage', + `fee_amount` bigint DEFAULT '0' COMMENT 'Fixed Fee Amount', + `enable` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Enabled', + `token` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Payment Token', + PRIMARY KEY (`id`), + UNIQUE KEY `uni_payment_token` (`token`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +-- ---------------------------- +-- Table structure for server +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `server` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Node Name', + `tags` varchar(128) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Tags', + `country` varchar(128) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Country', + `city` varchar(128) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'City', + `latitude` varchar(128) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'latitude', + `longitude` varchar(128) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'longitude', + `server_addr` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Server Address', + `relay_mode` varchar(20) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'none' COMMENT 'Relay Mode', + `relay_node` text COLLATE utf8mb4_general_ci COMMENT 'Relay Node', + `speed_limit` bigint NOT NULL DEFAULT '0' COMMENT 'Speed Limit', + `traffic_ratio` decimal(4,2) NOT NULL DEFAULT '0.00' COMMENT 'Traffic Ratio', + `group_id` bigint DEFAULT NULL COMMENT 'Group ID', + `protocol` varchar(20) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Protocol', + `config` text COLLATE utf8mb4_general_ci COMMENT 'Config', + `enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Enabled', + `sort` bigint NOT NULL DEFAULT '0' COMMENT 'Sort', + `last_reported_at` datetime(3) DEFAULT NULL COMMENT 'Last Reported Time', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + KEY `idx_group_id` (`group_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +-- ---------------------------- +-- Table structure for server_group +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `server_group` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Group Name', + `description` varchar(255) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Group Description', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +-- ---------------------------- +-- Table structure for sms +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `sms` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `content` text COLLATE utf8mb4_general_ci, + `platform` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL, + `area_code` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL, + `telephone` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL, + `status` tinyint(1) DEFAULT '1', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +-- ---------------------------- +-- Table structure for subscribe +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `subscribe` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Subscribe Name', + `description` text COLLATE utf8mb4_general_ci COMMENT 'Subscribe Description', + `unit_price` bigint NOT NULL DEFAULT '0' COMMENT 'Unit Price', + `unit_time` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Unit Time', + `discount` text COLLATE utf8mb4_general_ci COMMENT 'Discount', + `replacement` bigint NOT NULL DEFAULT '0' COMMENT 'Replacement', + `inventory` bigint NOT NULL DEFAULT '0' COMMENT 'Inventory', + `traffic` bigint NOT NULL DEFAULT '0' COMMENT 'Traffic', + `speed_limit` bigint NOT NULL DEFAULT '0' COMMENT 'Speed Limit', + `device_limit` bigint NOT NULL DEFAULT '0' COMMENT 'Device Limit', + `quota` bigint NOT NULL DEFAULT '0' COMMENT 'Quota', + `group_id` bigint DEFAULT NULL COMMENT 'Group Id', + `server_group` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Server Group', + `server` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Server', + `show` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Show portal page', + `sell` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Sell', + `sort` bigint NOT NULL DEFAULT '0' COMMENT 'Sort', + `deduction_ratio` bigint DEFAULT '0' COMMENT 'Deduction Ratio', + `allow_deduction` tinyint(1) DEFAULT '1' COMMENT 'Allow deduction', + `reset_cycle` bigint DEFAULT '0' COMMENT 'Reset Cycle: 0: No Reset, 1: 1st, 2: Monthly, 3: Yearly', + `renewal_reset` tinyint(1) DEFAULT '0' COMMENT 'Renew Reset', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +-- ---------------------------- +-- Table structure for subscribe_group +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `subscribe_group` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Group Name', + `description` text COLLATE utf8mb4_general_ci COMMENT 'Group Description', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +-- ---------------------------- +-- Table structure for subscribe_type +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `subscribe_type` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(50) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '订阅类型', + `mark` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '订阅标识', + `created_at` datetime(3) DEFAULT NULL COMMENT '创建时间', + `updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- ---------------------------- +-- Table structure for system +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `system` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `category` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Category', + `key` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Key Name', + `value` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Key Value', + `type` varchar(50) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Type', + `desc` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Description', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + UNIQUE KEY `uni_system_key` (`key`), + KEY `index_key` (`key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + + +-- ---------------------------- +-- Table structure for ticket +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `ticket` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `title` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Title', + `description` text COLLATE utf8mb4_general_ci COMMENT 'Description', + `user_id` bigint NOT NULL DEFAULT '0' COMMENT 'UserId', + `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Status', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- ---------------------------- +-- Table structure for ticket_follow +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `ticket_follow` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `ticket_id` bigint NOT NULL DEFAULT '0' COMMENT 'TicketId', + `from` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'From', + `type` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Type: 1 text, 2 image', + `content` text COLLATE utf8mb4_general_ci COMMENT 'Content', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- ---------------------------- +-- Table structure for traffic_log +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `traffic_log` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `server_id` bigint NOT NULL COMMENT 'Server ID', + `user_id` bigint NOT NULL COMMENT 'User ID', + `subscribe_id` bigint NOT NULL COMMENT 'Subscription ID', + `download` bigint DEFAULT '0' COMMENT 'Download Traffic', + `upload` bigint DEFAULT '0' COMMENT 'Upload Traffic', + `timestamp` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'Traffic Log Time', + PRIMARY KEY (`id`), + KEY `idx_subscribe_id` (`subscribe_id`), + KEY `idx_server_id` (`server_id`), + KEY `idx_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + + +-- ---------------------------- +-- Table structure for user +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `user` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `password` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'User Password', + `avatar` text COLLATE utf8mb4_general_ci COMMENT 'User Avatar', + `balance` bigint DEFAULT '0' COMMENT 'User Balance', + `telegram` bigint DEFAULT NULL COMMENT 'Telegram Account', + `refer_code` varchar(20) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Referral Code', + `referer_id` bigint DEFAULT NULL COMMENT 'Referrer ID', + `commission` bigint DEFAULT '0' COMMENT 'Commission', + `gift_amount` bigint DEFAULT '0' COMMENT 'User Gift Amount', + `enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Is Account Enabled', + `is_admin` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Admin', + `valid_email` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Email Verified', + `enable_email_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Email Notifications', + `enable_telegram_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Telegram Notifications', + `enable_balance_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Balance Change Notifications', + `enable_login_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Login Notifications', + `enable_subscribe_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Subscription Notifications', + `enable_trade_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Trade Notifications', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + `deleted_at` datetime(3) DEFAULT NULL COMMENT 'Deletion Time', + `is_del` bigint unsigned DEFAULT NULL COMMENT '1: Normal 0: Deleted', + PRIMARY KEY (`id`), + KEY `idx_referer` (`referer_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- ---------------------------- +-- Table structure for user_auth_methods +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `user_auth_methods` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `auth_type` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Auth Type 1: apple 2: google 3: github 4: facebook 5: telegram 6: email 7: phone', + `auth_identifier` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Auth Identifier', + `verified` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Verified', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + UNIQUE KEY `idx_auth_identifier` (`auth_identifier`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- ---------------------------- +-- Table structure for user_balance_log +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `user_balance_log` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `amount` bigint NOT NULL COMMENT 'Amount', + `type` tinyint(1) NOT NULL COMMENT 'Type: 1: Recharge 2: Withdraw 3: Payment 4: Refund 5: Reward', + `order_id` bigint DEFAULT NULL COMMENT 'Order ID', + `balance` bigint NOT NULL COMMENT 'Balance', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- ---------------------------- +-- Table structure for user_commission_log +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `user_commission_log` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `order_no` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Order No.', + `amount` bigint NOT NULL COMMENT 'Amount', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- ---------------------------- +-- Table structure for user_device +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `user_device` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `subscribe_id` bigint DEFAULT NULL COMMENT 'Subscribe ID', + `ip` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device Ip.', + `Identifier` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device Identifier.', + `user_agent` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device User Agent.', + `online` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Online', + `enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'EnableDeviceNumber', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- ---------------------------- +-- Table structure for user_gift_amount_log +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `user_gift_amount_log` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `user_subscribe_id` bigint DEFAULT NULL COMMENT 'Deduction User Subscribe ID', + `order_no` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Order No.', + `type` tinyint(1) NOT NULL COMMENT 'Type: 1: Increase 2: Reduce', + `amount` bigint NOT NULL COMMENT 'Amount', + `balance` bigint NOT NULL COMMENT 'Balance', + `remark` varchar(255) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Remark', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- ---------------------------- +-- Table structure for user_subscribe +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `user_subscribe` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `order_id` bigint NOT NULL COMMENT 'Order ID', + `subscribe_id` bigint NOT NULL COMMENT 'Subscription ID', + `start_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'Subscription Start Time', + `expire_time` datetime(3) DEFAULT NULL COMMENT 'Subscription Expire Time', + `traffic` bigint DEFAULT '0' COMMENT 'Traffic', + `download` bigint DEFAULT '0' COMMENT 'Download Traffic', + `upload` bigint DEFAULT '0' COMMENT 'Upload Traffic', + `token` varchar(255) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Token', + `uuid` varchar(255) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'UUID', + `status` tinyint(1) DEFAULT '0' COMMENT 'Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired 4: Deducted', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + `finished_at` datetime(3) DEFAULT NULL COMMENT 'Finished At', + PRIMARY KEY (`id`), + UNIQUE KEY `uni_user_subscribe_token` (`token`), + UNIQUE KEY `uni_user_subscribe_uuid` (`uuid`), + KEY `idx_user_id` (`user_id`), + KEY `idx_order_id` (`order_id`), + KEY `idx_subscribe_id` (`subscribe_id`), + KEY `idx_token` (`token`), + KEY `idx_uuid` (`uuid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `server_rule_group` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Rule Group Name', + `icon` text COLLATE utf8mb4_general_ci COMMENT 'Rule Group Icon', + `description` varchar(255) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Rule Group Description', + `enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Rule Group Enable', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + UNIQUE KEY `unique_name` (`name`) -- Add unique constraint to `name` +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + + +-- ---------------------------- +-- Table structure for user_login_log +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `user_login_log` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `login_ip` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Login IP', + `user_agent` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'UserAgent', + `success` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Login Success', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- ---------------------------- +-- Table structure for user_subscribe_log +-- ---------------------------- + +CREATE TABLE IF NOT EXISTS `user_subscribe_log` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `user_subscribe_id` bigint NOT NULL COMMENT 'User Subscribe ID', + `token` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Token', + `ip` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'IP', + `user_agent` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'UserAgent', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_user_subscribe_id` (`user_subscribe_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + + +CREATE TABLE IF NOT EXISTS `message_log` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `type` varchar(50) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'email' COMMENT 'Message Type', + `platform` varchar(50) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'smtp' COMMENT 'Platform', + `to` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'To', + `subject` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Subject', + `content` text COLLATE utf8mb4_general_ci COMMENT 'Content', + `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Status', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for user_device_online_record +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `user_device_online_record` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NULL DEFAULT NULL COMMENT 'User ID', + `identifier` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT 'Device Identifier', + `online_time` datetime(3) NULL DEFAULT NULL COMMENT 'Online Time', + `offline_time` datetime(3) NULL DEFAULT NULL COMMENT 'Offline Time', + `online_seconds` bigint NOT NULL DEFAULT '0' COMMENT 'Online Seconds ', + `duration_days` bigint NOT NULL DEFAULT '0' COMMENT 'Duration Days ', + `created_at` datetime(3) NULL DEFAULT NULL COMMENT 'Creation Time', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/initialize/migrate/init.go b/initialize/migrate/init.go new file mode 100644 index 0000000..3bfeba3 --- /dev/null +++ b/initialize/migrate/init.go @@ -0,0 +1,644 @@ +package migrate + +import ( + "fmt" + "time" + + "github.com/perfect-panel/ppanel-server/internal/model/auth" + "github.com/perfect-panel/ppanel-server/internal/model/payment" + "github.com/perfect-panel/ppanel-server/internal/model/subscribeType" + "github.com/perfect-panel/ppanel-server/internal/model/system" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/email" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/sms" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/uuidx" + "gorm.io/gorm" +) + +func InitPPanelSQL(db *gorm.DB) error { + logger.Info("PPanel SQL initialization started") + startTime := time.Now() + defer func() { + logger.Info("PPanel SQL initialization completed", logger.Field("duration", time.Since(startTime).String())) + + }() + return db.Transaction(func(tx *gorm.DB) error { + var err error + defer func() { + // If an error occurs, delete all tables + if err != nil { + logger.Debugf("PPanel SQL initialization completed, err: %v", err.Error()) + tables, _ := tx.Migrator().GetTables() + for _, table := range tables { + tx.Exec(fmt.Sprintf("DROP TABLE IF EXISTS `%s`", table)) + } + } + }() + // init ppanel.sql file + if err = ExecuteSQLFile(tx, "database/ppanel.sql"); err != nil { + return err + } + //Insert basic system data + if err = insertBasicSystemData(tx); err != nil { + return err + } + // insert into OAuth config + if err = insertAuthMethodConfig(tx); err != nil { + return err + } + // insert into Payment config + if err = insertPaymentConfig(tx); err != nil { + return err + } + // insert into SubscribeType + if err = insertSubscribeType(tx); err != nil { + return err + } + return err + }) +} + +func insertBasicSystemData(tx *gorm.DB) error { + if err := insertSiteConfig(tx); err != nil { + return err + } + if err := insertSubscribeConfig(tx); err != nil { + return err + } + if err := insertVerifyConfig(tx); err != nil { + return err + } + if err := insertSeverConfig(tx); err != nil { + return err + } + if err := insertInviteConfig(tx); err != nil { + return err + } + if err := insertRegisterConfig(tx); err != nil { + return err + } + if err := insertCurrencyConfig(tx); err != nil { + return err + } + if err := insertVerifyCodeConfig(tx); err != nil { + return err + } + + version := system.System{ + Category: "system", + Key: "Version", + Value: constant.Version, + Type: "string", + Desc: "System Version", + } + if err := tx.Model(&system.System{}).Save(&version).Error; err != nil { + return err + } + + return nil +} + +// insertSiteConfig +func insertSiteConfig(tx *gorm.DB) error { + siteConfig := []system.System{ + { + Category: "site", + Key: "SiteLogo", + Value: "/favicon.svg", + Type: "string", + Desc: "Site Logo", + }, + { + Category: "site", + Key: "SiteName", + Value: "Perfect Panel", + Type: "string", + Desc: "Site Name", + }, + { + Category: "site", + Key: "SiteDesc", + Value: "PPanel is a pure, professional, and perfect open-source proxy panel tool, designed to be your ideal choice for learning and practical use.", + Type: "string", + Desc: "Site Description", + }, + { + Category: "site", + Key: "Host", + Value: "", + Type: "string", + Desc: "Site Host", + }, + { + Category: "site", + Key: "Keywords", + Value: "Perfect Panel,PPanel", + Type: "string", + Desc: "Site Keywords", + }, + { + Category: "site", + Key: "CustomHTML", + Value: "", + Type: "string", + Desc: "Custom HTML", + }, + { + Category: "site", + Key: "CustomData", + Value: "{\"website\":\"\",\"contacts\":{\"email\":\"\",\"telephone\":\"\",\"address\":\"\"},\"community\":{\"telegram\":\"\",\"twitter\":\"\",\"discord\":\"\",\"instagram\":\"\",\"linkedin\":\"\",\"facebook\":\"\",\"github\":\"\"}}", + Type: "string", + Desc: "Custom data", + }, + { + Category: "tos", + Key: "TosContent", + Value: "Welcome to use Perfect Panel", + Type: "string", + Desc: "Terms of Service", + }, + { + Category: "tos", + Key: "PrivacyPolicy", + Value: "", + Type: "string", + Desc: "PrivacyPolicy", + }, + { + Category: "ad", + Key: "WebAD", + Value: "false", + Type: "bool", + Desc: "Display ad on the web", + }, + } + return tx.Model(&system.System{}).Save(&siteConfig).Error +} + +// insertSubscribeConfig +func insertSubscribeConfig(tx *gorm.DB) error { + subscribeConfig := []system.System{ + { + Category: "subscribe", + Key: "SingleModel", + Value: "false", + Type: "bool", + Desc: "是否单订阅模式", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + { + Category: "subscribe", + Key: "SubscribePath", + Value: "/api/subscribe", + Type: "string", + Desc: "订阅路径", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + { + Category: "subscribe", + Key: "SubscribeDomain", + Value: "", + Type: "string", + Desc: "订阅域名", + }, + { + Category: "subscribe", + Key: "PanDomain", + Value: "false", + Type: "bool", + Desc: "是否使用泛域名", + }, + } + return tx.Model(&system.System{}).Save(&subscribeConfig).Error +} + +// insertVerifyConfig +func insertVerifyConfig(tx *gorm.DB) error { + verifyConfig := []system.System{ + { + Category: "verify", + Key: "TurnstileSiteKey", + Value: "", + Type: "string", + Desc: "TurnstileSiteKey", + }, + { + Category: "verify", + Key: "TurnstileSecret", + Value: "", + Type: "string", + Desc: "TurnstileSecret", + }, + { + Category: "verify", + Key: "EnableLoginVerify", + Value: "false", + Type: "bool", + Desc: "is enable login verify", + }, + { + Category: "verify", + Key: "EnableRegisterVerify", + Value: "false", + Type: "bool", + Desc: "is enable register verify", + }, + { + Category: "verify", + Key: "EnableResetPasswordVerify", + Value: "false", + Type: "bool", + Desc: "is enable reset password verify", + }, + } + return tx.Model(&system.System{}).Save(&verifyConfig).Error +} + +// insertSeverConfig +func insertSeverConfig(tx *gorm.DB) error { + serverConfig := []system.System{ + { + Category: "server", + Key: "NodeSecret", + Value: "12345678", + Type: "string", + Desc: "node secret", + }, + { + Category: "server", + Key: "NodePullInterval", + Value: "10", + Type: "int", + Desc: "node pull interval", + }, + { + Category: "server", + Key: "NodePushInterval", + Value: "60", + Type: "int", + Desc: "node push interval", + }, + { + Category: "server", + Key: "NodeMultiplierConfig", + Value: "[]", + Type: "string", + Desc: "node multiplier config", + }, + } + return tx.Model(&system.System{}).Save(&serverConfig).Error +} + +// insertInviteConfig +func insertInviteConfig(tx *gorm.DB) error { + inviteConfig := []system.System{ + { + Category: "invite", + Key: "ForcedInvite", + Value: "false", + Type: "bool", + Desc: "Forced invite", + }, + { + Category: "invite", + Key: "ReferralPercentage", + Value: "20", + Type: "int", + Desc: "Referral percentage", + }, + { + Category: "invite", + Key: "OnlyFirstPurchase", + Value: "false", + Type: "bool", + Desc: "Only first purchase", + }, + } + return tx.Model(&system.System{}).Save(&inviteConfig).Error +} + +// insertRegisterConfig +func insertRegisterConfig(tx *gorm.DB) error { + registerConfig := []system.System{ + { + Category: "register", + Key: "StopRegister", + Value: "false", + Type: "bool", + Desc: "is stop register", + }, + { + Category: "register", + Key: "EnableTrial", + Value: "false", + Type: "bool", + Desc: "is enable trial", + }, + { + Category: "register", + Key: "TrialSubscribe", + Value: "", + Type: "int", + Desc: "Trial subscription", + }, + { + Category: "register", + Key: "TrialTime", + Value: "24", + Type: "int", + Desc: "Trial time", + }, + { + Category: "register", + Key: "TrialTimeUnit", + Value: "Hour", + Type: "string", + Desc: "Trial time unit", + }, + { + Category: "register", + Key: "EnableIpRegisterLimit", + Value: "false", + Type: "bool", + Desc: "is enable IP register limit", + }, + { + Category: "register", + Key: "IpRegisterLimit", + Value: "3", + Type: "int", + Desc: "IP Register Limit", + }, + { + Category: "register", + Key: "IpRegisterLimitDuration", + Value: "64", + Type: "int", + Desc: "IP Register Limit Duration (minutes)", + }, + } + return tx.Model(&system.System{}).Save(®isterConfig).Error +} + +// insertAuthMethodConfig +func insertAuthMethodConfig(tx *gorm.DB) error { + // insert into OAuth config + var methods []auth.Auth + methods = append(methods, []auth.Auth{ + initEmailConfig(), + initMobileConfig(), + { + Method: "apple", + Config: new(auth.AppleAuthConfig).Marshal(), + }, + { + Method: "google", + Config: new(auth.GoogleAuthConfig).Marshal(), + }, + { + Method: "github", + Config: new(auth.GithubAuthConfig).Marshal(), + }, + { + Method: "facebook", + Config: new(auth.FacebookAuthConfig).Marshal(), + }, + { + + Method: "telegram", + Config: new(auth.TelegramAuthConfig).Marshal(), + }, + { + Method: "device", + Config: new(auth.DeviceConfig).Marshal(), + }, + }...) + return tx.Model(&auth.Auth{}).Save(&methods).Error +} + +// insertPaymentConfig +func insertPaymentConfig(tx *gorm.DB) error { + enable := true + payments := []payment.Payment{ + { + Id: -1, + Name: "Balance", + Platform: "balance", + Icon: "", + Domain: "", + Config: "", + FeeMode: 0, + FeePercent: 0, + FeeAmount: 0, + Enable: &enable, + }, + } + // reset auto increment + if err := tx.Exec("ALTER TABLE `payment` AUTO_INCREMENT = 1").Error; err != nil { + logger.Errorw("Reset auto increment failed", logger.Field("error", err)) + return err + } + return tx.Model(&payment.Payment{}).Save(&payments).Error +} + +// insertSubscribeType +func insertSubscribeType(tx *gorm.DB) error { + // insert into subscribe type + var subscribeTypes []subscribeType.SubscribeType + subscribeTypes = append(subscribeTypes, []subscribeType.SubscribeType{ + { + Name: "Clash", + Mark: "Clash", + }, + { + Name: "Hiddify", + Mark: "Hiddify", + }, + { + Name: "Loon", + Mark: "Loon", + }, + { + Name: "NekoBox", + Mark: "NekoBox", + }, + { + Name: "NekoRay", + Mark: "NekoRay", + }, + { + Name: "Netch", + Mark: "Netch", + }, + { + Name: "Quantumult", + Mark: "Quantumult", + }, + { + Name: "Shadowrocket", + Mark: "Shadowrocket", + }, + { + Name: "Singbox", + Mark: "Singbox", + }, + { + Name: "Surfboard", + Mark: "Surfboard", + }, + { + Name: "Surge", + Mark: "Surge", + }, + { + Name: "V2box", + Mark: "V2box", + }, + { + Name: "V2rayN", + Mark: "V2rayN", + }, + { + Name: "V2rayNg", + Mark: "V2rayNg", + }, + }...) + // insert into payment + return tx.Save(&subscribeTypes).Error +} + +// CreateAdminUser create admin user +func CreateAdminUser(email, password string, tx *gorm.DB) error { + enable := true + return tx.Transaction(func(tx *gorm.DB) error { + // Prevent duplicate creation + if tx.Model(&user.User{}).Find(&user.User{}).RowsAffected != 0 { + logger.Info("User already exists, skip creating administrator account") + return nil + } + + u := user.User{ + Password: tool.EncodePassWord(password), + IsAdmin: &enable, + ReferCode: uuidx.UserInviteCode(time.Now().Unix()), + } + if err := tx.Model(&user.User{}).Save(&u).Error; err != nil { + return err + } + method := user.AuthMethods{ + UserId: u.Id, + AuthType: "email", + AuthIdentifier: email, + Verified: true, + } + if err := tx.Model(&user.AuthMethods{}).Save(&method).Error; err != nil { + return err + } + return nil + }) +} + +func initEmailConfig() auth.Auth { + enable := true + smtpConfig := new(auth.SMTPConfig) + emailConfig := auth.EmailAuthConfig{ + Platform: "smtp", + PlatformConfig: smtpConfig, + EnableVerify: false, + EnableDomainSuffix: false, + DomainSuffixList: "", + VerifyEmailTemplate: email.DefaultEmailVerifyTemplate, + ExpirationEmailTemplate: email.DefaultExpirationEmailTemplate, + MaintenanceEmailTemplate: email.DefaultMaintenanceEmailTemplate, + TrafficExceedEmailTemplate: email.DefaultTrafficExceedEmailTemplate, + } + authMethod := auth.Auth{ + Method: "email", + Config: emailConfig.Marshal(), + Enabled: &enable, + } + return authMethod +} + +func initMobileConfig() auth.Auth { + cfg := new(auth.AlibabaCloudConfig) + mobileConfig := auth.MobileAuthConfig{ + Platform: sms.AlibabaCloud.String(), + PlatformConfig: cfg, + EnableWhitelist: false, + Whitelist: make([]string, 0), + } + authMethod := auth.Auth{ + Method: "mobile", + Config: mobileConfig.Marshal(), + } + return authMethod +} + +// insert into currency config +func insertCurrencyConfig(tx *gorm.DB) error { + currencyConfig := []system.System{ + { + Category: "currency", + Key: "Currency", + Value: "USD", + Type: "string", + Desc: "Currency", + }, + { + Category: "currency", + Key: "CurrencySymbol", + Value: "$", + Type: "string", + Desc: "Currency Symbol", + }, + { + Category: "currency", + Key: "CurrencyUnit", + Value: "USD", + Type: "string", + Desc: "Currency Unit", + }, + { + Category: "currency", + Key: "AccessKey", + Value: "", + Type: "string", + Desc: "Exchangerate Access Key", + }, + } + return tx.Model(&system.System{}).Save(¤cyConfig).Error +} + +// insert into verify code config +func insertVerifyCodeConfig(tx *gorm.DB) error { + verifyCodeConfig := []system.System{ + { + Category: "verify_code", + Key: "VerifyCodeExpireTime", + Value: "300", + Type: "int", + Desc: "Verify code expire time", + }, + { + Category: "verify_code", + Key: "VerifyCodeLimit", + Value: "15", + Type: "int", + Desc: "limits of verify code", + }, + { + Category: "verify_code", + Key: "VerifyCodeInterval", + Value: "60", + Type: "int", + Desc: "Interval of verify code", + }, + } + return tx.Model(&system.System{}).Save(&verifyCodeConfig).Error +} diff --git a/initialize/migrate/init_test.go b/initialize/migrate/init_test.go new file mode 100644 index 0000000..183c3f5 --- /dev/null +++ b/initialize/migrate/init_test.go @@ -0,0 +1,37 @@ +package migrate + +import ( + "testing" + + "github.com/perfect-panel/ppanel-server/pkg/orm" + "gorm.io/gorm" +) + +func connMySQL() *gorm.DB { + + cfg := orm.Config{ + Addr: "127.0.0.1", + Username: "root", + Password: "mylove520", + Dbname: "ppanel", + } + db, err := orm.ConnectMysql(orm.Mysql{ + Config: cfg, + }) + if err != nil { + return nil + } + return db +} +func TestInitPPanelSQL(t *testing.T) { + t.Skipf("Skip TestInitPPanelSQL") + db := connMySQL() + if db == nil { + t.Error("connect mysql failed") + return + } + if err := InitPPanelSQL(db); err != nil { + t.Error(err) + } + t.Logf("InitPPanelSQL success") +} diff --git a/initialize/migrate/migrate.go b/initialize/migrate/migrate.go new file mode 100644 index 0000000..1e227fd --- /dev/null +++ b/initialize/migrate/migrate.go @@ -0,0 +1,34 @@ +package migrate + +import ( + "embed" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "github.com/perfect-panel/ppanel-server/internal/model/system" + "github.com/perfect-panel/ppanel-server/internal/svc" +) + +//go:embed database/*.sql +var sqlFiles embed.FS + +func Migrate(ctx *svc.ServiceContext) { + logger.Debug("SQL Migrate started") + startTime := time.Now() + defer func() { + logger.WithDuration(time.Since(startTime)).Debug("PPanel SQL Migrate completed") + }() + db := ctx.DB + if !db.Migrator().HasTable(&system.System{}) { + if err := InitPPanelSQL(db); err != nil { + logger.Error("SQL Migrate failed", logger.Field("err", err.Error())) + panic(err) + } + // create admin user + if err := CreateAdminUser(ctx.Config.Administrator.Email, ctx.Config.Administrator.Password, db); err != nil { + logger.Error("Create admin User failed", logger.Field("err", err.Error())) + panic(err) + } + } +} diff --git a/initialize/migrate/patch/01703.go b/initialize/migrate/patch/01703.go new file mode 100644 index 0000000..b49392f --- /dev/null +++ b/initialize/migrate/patch/01703.go @@ -0,0 +1,456 @@ +package patch + +import ( + "github.com/perfect-panel/ppanel-server/initialize/migrate" + "github.com/perfect-panel/ppanel-server/internal/model/application" + "github.com/perfect-panel/ppanel-server/internal/model/auth" + "github.com/perfect-panel/ppanel-server/internal/model/log" + "github.com/perfect-panel/ppanel-server/internal/model/server" + "github.com/perfect-panel/ppanel-server/internal/model/system" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/email" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/sms" + "gorm.io/gorm" +) + +func Migrate01200(db *gorm.DB) error { + var version = "0.1.2(01200)" + return db.Transaction(func(tx *gorm.DB) error { + if exists := db.Migrator().HasColumn(&user.OldUser{}, "email"); !exists { + logger.Debug("Migrate 01200 skipped", logger.Field("reason", "old user table not exists")) + return nil + } + + logger.Debug("Migrate 01200 started", logger.Field("step", "1"), logger.Field("action", "migrate old user to user auth methods")) + var users []*user.OldUser + if err := tx.Model(&user.OldUser{}).Find(&users).Error; err != nil { + return err + } + if err := tx.Migrator().AutoMigrate(&user.AuthMethods{}); err != nil { + logger.Errorw("Migrate 01200 failed", logger.Field("step", "1"), logger.Field("action", "create user auth methods table"), logger.Field("error", err.Error())) + return err + } + err := tx.Transaction(func(tx *gorm.DB) error { + for _, oldUser := range users { + if oldUser.Email == "" { + continue + } + // create user auth method + authMethod := &user.AuthMethods{ + UserId: oldUser.Id, + AuthType: "email", + AuthIdentifier: oldUser.Email, + Verified: false, + } + if err := tx.Create(authMethod).Error; err != nil { + return err + } + } + return nil + }) + if err != nil { + logger.Errorw("Migrate 01200 failed", logger.Field("step", "1"), logger.Field("action", "migrate old user to user auth methods"), logger.Field("error", err.Error())) + return err + } + logger.Debug("Migrate 01200 completed", logger.Field("step", "1"), logger.Field("action", "migrate old user to user auth methods")) + + logger.Debug("Migrate 01200 started", logger.Field("step", "2"), logger.Field("action", "exclude sql files")) + // exclude sql files + if err := migrate.ExecuteSQLFile(tx, "database/01200-patch.sql"); err != nil { + logger.Errorw("Migrate 01200 failed", logger.Field("step", "2"), logger.Field("action", "exclude sql files"), logger.Field("file", "database/01200-patch.sql"), logger.Field("error", err.Error())) + return err + } + logger.Debug("Migrate 01200 completed", logger.Field("step", "2"), logger.Field("action", "exclude sql files")) + + logger.Debug("Migrate 01200 started", logger.Field("step", "3"), logger.Field("action", "update system config")) + versionConfig := &system.System{ + Category: "system", + Key: "Version", + Value: version, + Type: "string", + Desc: "Version of the system, eg: 1.0.0(10000)", + } + // update system config + if err := tx.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Save(&versionConfig).Error; err != nil { + logger.Errorw("Migrate 01200 failed", logger.Field("step", "3"), logger.Field("action", "update system config"), logger.Field("error", err.Error())) + return err + } + return nil + }) +} + +func Migrate01201(db *gorm.DB) error { + version := "0.1.2(01201)" + // exclude sql files + if err := migrate.ExecuteSQLFile(db, "database/01201-patch.sql"); err != nil { + logger.Errorw("Migrate 01201 failed", logger.Field("step", "1"), logger.Field("action", "exclude sql files"), logger.Field("file", "database/01200-patch.sql"), logger.Field("error", err.Error())) + return err + } + // update system config + if err := db.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error; err != nil { + logger.Errorw("Migrate 01201 failed", logger.Field("step", "2"), logger.Field("action", "update system config"), logger.Field("error", err.Error())) + return err + } + return nil +} + +func Migrate01202(db *gorm.DB) error { + version := "0.1.2(01202)" + return db.Transaction(func(tx *gorm.DB) error { + // migrate email config to system config + if err := db.Migrator().AutoMigrate(&auth.Auth{}); err != nil { + logger.Errorw("Migrate01202: AutoMigrate Auth failed", logger.Field("version", version), logger.Field("error", err.Error())) + return err + } + if db.Migrator().HasColumn("oauth_config", "platform") { + if err := db.Migrator().RenameColumn("oauth_config", "platform", "method"); err != nil { + logger.Errorw("Migrate01202: RenameColumn platform to method failed", logger.Field("version", version), logger.Field("error", err.Error())) + } + } + + // init email config + if err := initEmailConfig(db); err != nil { + logger.Errorw("Migrate01202: initEmailConfig failed", logger.Field("version", version), logger.Field("error", err.Error())) + return err + } + // init mobile config + if err := initMobileConfig(db); err != nil { + logger.Errorw("Migrate01202: initMobileConfig failed", logger.Field("version", version), logger.Field("error", err.Error())) + return err + } + // drop oauth_config table + err := db.Migrator().DropTable("oauth_config") + if err != nil { + logger.Debug("Migrate01202: DropTable oauth_config failed", logger.Field("version", version), logger.Field("error", err.Error())) + } + // exclude sql files + if err := migrate.ExecuteSQLFile(db, "database/01202-patch.sql"); err != nil { + logger.Errorw("Migrate 01202 failed", logger.Field("action", "exclude sql files"), logger.Field("file", "database/012002-patch.sql"), logger.Field("error", err.Error())) + return err + } + + // update system config + if err := db.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error; err != nil { + logger.Errorw("Migrate 01202 failed", logger.Field("step", "2"), logger.Field("action", "update system config"), logger.Field("error", err.Error())) + return err + } + return nil + }) +} +func Migrate01203(db *gorm.DB) error { + version := "0.1.2(01203)" + return db.Transaction(func(tx *gorm.DB) error { + if err := db.AutoMigrate(&user.LoginLog{}, &user.SubscribeLog{}); err != nil { + logger.Errorw("Migrate01203: AutoMigrate LoginLog/SubscribeLog failed", logger.Field("version", version), logger.Field("error", err.Error())) + return err + } + // update version + if err := db.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error; err != nil { + logger.Errorw("Migrate01203: Update Version failed", logger.Field("version", version), logger.Field("error", err.Error())) + return err + } + return nil + }) +} + +func Migrate01204(db *gorm.DB) error { + version := "0.1.2(01204)" + return db.Transaction(func(tx *gorm.DB) error { + if err := db.AutoMigrate(&log.MessageLog{}); err != nil { + logger.Errorw("Migrate01204: AutoMigrate MessageLog failed", logger.Field("version", version), logger.Field("error", err.Error())) + return err + } + // Trial configuration + if err := initTrialConfig(tx); err != nil { + logger.Errorw("Migrate01204: initTrialConfig failed", logger.Field("version", version), logger.Field("error", err.Error())) + return err + } + // Add auth method with device + if err := addAuthMethodWithDevice(tx); err != nil { + logger.Errorw("Migrate01204: Add auth method with device failed", logger.Field("version", version), logger.Field("error", err.Error())) + return err + } + // update version + if err := db.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error; err != nil { + logger.Errorw("Migrate01204: Update Version failed", logger.Field("version", version), logger.Field("error", err.Error())) + return err + } + + return nil + }) +} + +func Migrate01205(db *gorm.DB) error { + version := "0.1.2(01205)" + return db.Transaction(func(tx *gorm.DB) error { + // Add VerifyCode public configuration + configs := []system.System{ + { + Category: "verify_code", + Key: "VerifyCodeExpireTime", + Value: "5", + Type: "int", + Desc: "Verify code expire time", + }, + { + Category: "verify_code", + Key: "VerifyCodeLimit", + Value: "15", + Type: "int", + Desc: "limits of verify code", + }, + { + Category: "verify_code", + Key: "VerifyCodeInterval", + Value: "60", + Type: "int", + Desc: "Interval of verify code", + }, + } + if err := tx.Model(&system.System{}).Save(&configs).Error; err != nil { + logger.Errorw("Migrate01205: Save VerifyCode public configuration failed", logger.Field("error", err.Error())) + return err + } + + // update version + if err := db.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error; err != nil { + logger.Errorw("Migrate01205: Update Version failed", logger.Field("version", version), logger.Field("error", err.Error())) + return err + } + return nil + }) +} + +func Migrate01301(db *gorm.DB) error { + version := "0.1.3(01301)" + return db.Transaction(func(tx *gorm.DB) error { + err := tx.Migrator().AlterColumn(&application.Application{}, "icon") + if err != nil { + logger.Errorw("Migrate01301: AlterColumn failed", logger.Field("error", err.Error())) + return err + } + // update version + if err := db.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error; err != nil { + logger.Errorw("Migrate01205: Update Version failed", logger.Field("version", version), logger.Field("error", err.Error())) + return err + } + return nil + }) +} + +func Migrate01602(db *gorm.DB) error { + version := "0.1.6(01602)" + return db.Transaction(func(tx *gorm.DB) error { + if tx.Model(&system.System{}).Where("`category` = 'tos' AND `key` = 'TosContent'").Find(&system.System{}).RowsAffected == 0 { + if err := tx.Save(&system.System{ + Category: "tos", + Key: "TosContent", + Value: "Welcome to use Perfect Panel", + Type: "string", + Desc: "Terms of Service", + }).Error; err != nil { + return err + } + } + // update version + if err := tx.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error; err != nil { + return err + } + return nil + }) +} +func Migrate01701(db *gorm.DB) error { + return db.Transaction(func(tx *gorm.DB) error { + version := "0.1.7(01701)" + if err := db.Migrator().AlterColumn(&user.User{}, "Avatar"); err != nil { + return err + } + // update version + if err := tx.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error; err != nil { + return err + } + return nil + }) + +} + +func Migrate01702(db *gorm.DB) error { + return db.Transaction(func(tx *gorm.DB) error { + version := "0.1.7(01702)" + + if tx.Model(&system.System{}).Where("`category` = 'site' AND `key` = 'Keywords'").Find(&system.System{}).RowsAffected == 0 { + if err := tx.Save(&system.System{ + Category: "site", + Key: "Keywords", + Value: "Perfect Panel,PPanel", + Type: "string", + Desc: "Keywords", + }).Error; err != nil { + return err + } + } + if tx.Model(&system.System{}).Where("`category` = 'site' AND `key` = 'CustomHTML'").Find(&system.System{}).RowsAffected == 0 { + if err := tx.Save(&system.System{ + Category: "site", + Key: "CustomHTML", + Value: "", + Type: "string", + Desc: "Custom HTML", + }).Error; err != nil { + return err + } + } + + // update version + if err := tx.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error; err != nil { + return err + } + return nil + }) +} + +func Migrate01703(db *gorm.DB) error { + version := "0.1.7(01703)" + return db.Transaction(func(tx *gorm.DB) error { + if tx.Model(&system.System{}).Where("`category` = 'tos' AND `key` = 'PrivacyPolicy'").Find(&system.System{}).RowsAffected == 0 { + if err := tx.Save(&system.System{ + Category: "tos", + Key: "PrivacyPolicy", + Value: "", + Type: "string", + Desc: "Privacy Policy", + }).Error; err != nil { + return err + } + } + return tx.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error + }) +} +func Migrate01704(db *gorm.DB) error { + version := "0.1.7(01704)" + + // check server table latitude column exists, if not exists, create it + if exists := db.Migrator().HasColumn(&server.Server{}, "latitude"); !exists { + if err := db.Migrator().AddColumn(&server.Server{}, "latitude"); err != nil { + logger.Errorw("Migrate 01704 failed", logger.Field("action", "add latitude column"), logger.Field("error", err.Error())) + return err + } + logger.Infow("Migrate 01704 success", logger.Field("action", "add latitude column")) + } + // check server table longitude column exists, if not exists, create it + if exists := db.Migrator().HasColumn(&server.Server{}, "longitude"); !exists { + if err := db.Migrator().AddColumn(&server.Server{}, "longitude"); err != nil { + logger.Errorw("Migrate 01704 failed", logger.Field("action", "add longitude column"), logger.Field("error", err.Error())) + return err + } + logger.Infow("Migrate 01704 success", logger.Field("action", "add longitude column")) + } + // update system config + if err := db.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error; err != nil { + logger.Errorw("Migrate 01704 failed", logger.Field("step", "2"), logger.Field("action", "update system config"), logger.Field("error", err.Error())) + return err + } + logger.Infow("Migrate 01704 success", logger.Field("action", "update system config")) + return nil +} + +func Migrate01705(db *gorm.DB) error { + version := "0.1.7(01705)" + // check user_device table exists, if not exists, create it + if exists := db.Migrator().HasTable(&user.Device{}); !exists { + if err := db.Migrator().CreateTable(&user.Device{}); err != nil { + logger.Errorw("Migrate 01705 failed", logger.Field("action", "create user_device table"), logger.Field("error", err.Error())) + return err + } + logger.Infow("Migrate 01705 success", logger.Field("action", "create user_device table")) + } + // check user_table exists and imei column exists, if exists, update imei column name to identifier + if exists := db.Migrator().HasColumn(&user.Device{}, "imei"); exists { + if err := db.Migrator().RenameColumn(&user.Device{}, "imei", "identifier"); err != nil { + logger.Errorw("Migrate 01705 failed", logger.Field("action", "rename imei column to identifier"), logger.Field("error", err.Error())) + return err + } + logger.Infow("Migrate 01705 success", logger.Field("action", "rename imei column to identifier")) + } + + // update system config + if err := db.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error; err != nil { + logger.Errorw("Migrate 01705 failed", logger.Field("step", "2"), logger.Field("action", "update system config"), logger.Field("error", err.Error())) + return err + } + return nil +} + +func initMobileConfig(db *gorm.DB) error { + cfg := new(auth.AlibabaCloudConfig) + mobileConfig := auth.MobileAuthConfig{ + Platform: sms.AlibabaCloud.String(), + PlatformConfig: cfg.Marshal(), + EnableWhitelist: false, + Whitelist: make([]string, 0), + } + authMethod := auth.Auth{ + Method: "mobile", + Config: mobileConfig.Marshal(), + } + if err := db.Save(&authMethod).Error; err != nil { + return err + } + return nil +} + +func initEmailConfig(db *gorm.DB) error { + enable := true + smtpConfig := new(auth.SMTPConfig) + + emailConfig := auth.EmailAuthConfig{ + Platform: "smtp", + PlatformConfig: smtpConfig.Marshal(), + EnableVerify: false, + EnableDomainSuffix: false, + DomainSuffixList: "", + VerifyEmailTemplate: email.DefaultEmailVerifyTemplate, + ExpirationEmailTemplate: email.DefaultExpirationEmailTemplate, + MaintenanceEmailTemplate: email.DefaultMaintenanceEmailTemplate, + } + authMethod := auth.Auth{ + Method: "email", + Config: emailConfig.Marshal(), + Enabled: &enable, + } + return db.Save(&authMethod).Error +} + +func initTrialConfig(tx *gorm.DB) error { + configs := []system.System{ + { + Category: "register", + Key: "TrialSubscribe", + Value: "", + Type: "int", + Desc: "Trial subscription", + }, + { + Category: "register", + Key: "TrialTime", + Value: "24", + Type: "int", + Desc: "Trial time", + }, + { + Category: "register", + Key: "TrialTimeUnit", + Value: "Hour", + Type: "string", + Desc: "Trial time unit", + }, + } + return tx.Model(&system.System{}).Save(&configs).Error +} + +func addAuthMethodWithDevice(tx *gorm.DB) error { + return tx.Model(&auth.Auth{}).Save(&auth.Auth{ + Method: "device", + }).Error +} diff --git a/initialize/migrate/patch/02000.go b/initialize/migrate/patch/02000.go new file mode 100644 index 0000000..cf67ebb --- /dev/null +++ b/initialize/migrate/patch/02000.go @@ -0,0 +1,252 @@ +package patch + +import ( + "github.com/perfect-panel/ppanel-server/internal/model/ads" + "github.com/perfect-panel/ppanel-server/internal/model/application" + "github.com/perfect-panel/ppanel-server/internal/model/auth" + "github.com/perfect-panel/ppanel-server/internal/model/order" + "github.com/perfect-panel/ppanel-server/internal/model/payment" + "github.com/perfect-panel/ppanel-server/internal/model/server" + "github.com/perfect-panel/ppanel-server/internal/model/system" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "gorm.io/gorm" +) + +func Migrate02000(db *gorm.DB) error { + version := "0.2.0(02000)" + return db.Transaction(func(tx *gorm.DB) error { + if err := initDeviceConfig(tx); err != nil { + logMigrationError("Setting Device Config", err) + return err + } + logMigrationSuccess("Setting Device Config") + + if !tx.Migrator().HasTable(&ads.Ads{}) { + if err := createAdsTable(tx); err != nil { + return err + } + } + + if err := updatePaymentTable(tx); err != nil { + return err + } + + if err := tx.Migrator().AutoMigrate(&order.Order{}); err != nil { + logMigrationError("Auto Migrate Order", err) + return err + } + + return updateSystemVersion(tx, version) + }) +} + +func Migrate02001(db *gorm.DB) error { + version := "0.2.0(02001)" + return db.Transaction(func(tx *gorm.DB) error { + if tx.Model(&system.System{}).Where("`category` = 'site' AND `key` = 'CustomData'").Find(&system.System{}).RowsAffected == 0 { + if err := tx.Save(&system.System{ + Category: "site", + Key: "CustomData", + Value: "{\"website\":\"\",\"contacts\":{\"email\":\"\",\"telephone\":\"\",\"address\":\"\"},\"community\":{\"telegram\":\"\",\"twitter\":\"\",\"discord\":\"\",\"instagram\":\"\",\"linkedin\":\"\",\"facebook\":\"\",\"github\":\"\"}}", + Type: "string", + Desc: "Custom data", + }).Error; err != nil { + logMigrationError("create custom data system config", err) + return err + } + } + return updateSystemVersion(tx, version) + }) +} + +func Migrate02002(db *gorm.DB) error { + version := "0.2.0(02002)" + return db.Transaction(func(tx *gorm.DB) error { + if err := tx.Model(&system.System{}).Where("`category` = 'site' AND `key` = 'CustomData'").UpdateColumn("type", "string").Error; err != nil { + return err + } + return updateSystemVersion(tx, version) + }) +} + +func Migrate02003(db *gorm.DB) error { + version := "0.2.0(02003)" + return db.Transaction(func(tx *gorm.DB) error { + if err := addColumnIfNotExists(tx, &order.Order{}, "payment_id"); err != nil { + return err + } + if err := addColumnIfNotExists(tx, &payment.Payment{}, "platform"); err != nil { + return err + } + if err := dropColumnIfExists(tx, &payment.Payment{}, "mark"); err != nil { + return err + } + if err := addColumnIfNotExists(tx, &payment.Payment{}, "description"); err != nil { + return err + } + if err := addColumnIfNotExists(tx, &payment.Payment{}, "token"); err != nil { + return err + } + return updateSystemVersion(tx, version) + }) +} + +func Migrate02007(db *gorm.DB) error { + version := "0.2.0(02007)" + return db.Transaction(func(tx *gorm.DB) error { + if err := recreateTable(tx, &server.RuleGroup{}); err != nil { + return err + } + return updateSystemVersion(tx, version) + }) +} + +func Migrate02008(db *gorm.DB) error { + version := "0.2.0(02008)" + return db.Transaction(func(tx *gorm.DB) error { + if exists := tx.Migrator().HasColumn(&application.ApplicationConfig{}, "invitation_link"); !exists { + if err := tx.Migrator().AddColumn(&application.ApplicationConfig{}, "invitation_link"); err != nil { + logger.Errorw("Migrate 02008 failed", logger.Field("action", "add invitation_link column"), logger.Field("error", err.Error())) + return err + } + logger.Infow("Migrate 02008 success", logger.Field("action", "add invitation_link column")) + } + + if exists := tx.Migrator().HasTable(&user.DeviceOnlineRecord{}); !exists { + if err := tx.Migrator().CreateTable(&user.DeviceOnlineRecord{}); err != nil { + logger.Errorw("Migrate 02008 failed", logger.Field("action", "create device_online_record table"), logger.Field("error", err.Error())) + return err + } + } + return tx.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error + }) +} + +func Migrate02009(db *gorm.DB) error { + version := "0.2.0(02009)" + return db.Transaction(func(tx *gorm.DB) error { + if err := addColumnIfNotExists(tx, &user.Subscribe{}, "finished_at"); err != nil { + logger.Errorw("Migrate 02009 failed", logger.Field("action", "subscribe table add finished_at column"), logger.Field("error", err.Error())) + return err + } + return updateSystemVersion(tx, version) + }) +} + +func Migrate02010(db *gorm.DB) error { + version := "0.2.0(02010)" + return db.Transaction(func(tx *gorm.DB) error { + if err := addColumnIfNotExists(tx, &application.ApplicationConfig{}, "kr_website_id"); err != nil { + logger.Errorw("Migrate 02010 failed", logger.Field("action", "application_config table add kr_website_id column"), logger.Field("error", err.Error())) + return err + } + return updateSystemVersion(tx, version) + }) +} + +func Migrate02011(db *gorm.DB) error { + version := "0.2.0(02011)" + return db.Transaction(func(tx *gorm.DB) error { + if err := addColumnIfNotExists(tx, &user.Subscribe{}, "used_period"); err != nil { + logger.Errorw("Migrate 02011 failed", logger.Field("action", "user.Subscribe table add used_period column"), logger.Field("error", err.Error())) + return err + } + if err := addColumnIfNotExists(tx, &user.Subscribe{}, "total_period"); err != nil { + logger.Errorw("Migrate 02011 failed", logger.Field("action", "user.Subscribe table add total_period column"), logger.Field("error", err.Error())) + return err + } + return updateSystemVersion(tx, version) + }) +} + +func initDeviceConfig(db *gorm.DB) error { + cfg := new(auth.DeviceConfig) + return db.Model(&auth.Auth{}).Where("method = ?", "device").Update("config", cfg.Marshal()).Error +} + +func createAdsTable(tx *gorm.DB) error { + if err := tx.Migrator().CreateTable(&ads.Ads{}); err != nil { + logMigrationError("Create Table Ads", err) + return err + } + logMigrationSuccess("Create Table Ads") + return tx.Model(&system.System{}).Save(&system.System{ + Category: "ad", + Key: "WebAD", + Value: "false", + Type: "bool", + Desc: "Display ad on the web", + }).Error +} + +func updatePaymentTable(tx *gorm.DB) error { + if err := tx.Exec("DROP TABLE IF EXISTS `payment`").Error; err != nil { + logMigrationError("Drop Payment Table", err) + } + if err := tx.AutoMigrate(&payment.Payment{}); err != nil { + logMigrationError("Auto Migrate Payment", err) + return err + } + enable := true + return tx.Model(&payment.Payment{}).Create(&payment.Payment{ + Id: -1, + Name: "", + Platform: "balance", + Icon: "", + Domain: "", + Config: "", + FeeMode: 0, + FeePercent: 0, + FeeAmount: 0, + Enable: &enable, + }).Error +} + +func updateSystemVersion(tx *gorm.DB, version string) error { + return tx.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error +} + +func addColumnIfNotExists(tx *gorm.DB, model interface{}, columnName string) error { + if exists := tx.Migrator().HasColumn(model, columnName); !exists { + if err := tx.Migrator().AddColumn(model, columnName); err != nil { + logMigrationError("add "+columnName+" column", err) + return err + } + logMigrationSuccess("add " + columnName + " column") + } + return nil +} + +func dropColumnIfExists(tx *gorm.DB, model interface{}, columnName string) error { + if exists := tx.Migrator().HasColumn(model, columnName); exists { + if err := tx.Migrator().DropColumn(model, columnName); err != nil { + logMigrationError("del "+columnName+" column", err) + return err + } + logMigrationSuccess("del " + columnName + " column") + } + return nil +} + +func recreateTable(tx *gorm.DB, model interface{}) error { + if exists := tx.Migrator().HasTable(model); exists { + if err := tx.Migrator().DropTable(model); err != nil { + logMigrationError("drop table", err) + return err + } + } + if err := tx.Migrator().CreateTable(model); err != nil { + logMigrationError("create table", err) + return err + } + return nil +} + +func logMigrationError(action string, err error) { + logger.Errorw("Migration failed", logger.Field("action", action), logger.Field("error", err.Error())) +} + +func logMigrationSuccess(action string) { + logger.Infow("Migration success", logger.Field("action", action)) +} diff --git a/initialize/migrate/patch/03001.go b/initialize/migrate/patch/03001.go new file mode 100644 index 0000000..144ba32 --- /dev/null +++ b/initialize/migrate/patch/03001.go @@ -0,0 +1,30 @@ +package patch + +import ( + "github.com/perfect-panel/ppanel-server/internal/model/application" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "gorm.io/gorm" +) + +func Migrate03001(db *gorm.DB) error { + version := "0.3.0(1)" + return db.Transaction(func(tx *gorm.DB) error { + if err := addColumnIfNotExists(tx, &user.Subscribe{}, "finished_at"); err != nil { + logger.Errorw("Migrate 03001 failed", logger.Field("action", "user.Subscribe table add finished_at column"), logger.Field("error", err.Error())) + return err + } + return updateSystemVersion(tx, version) + }) +} + +func Migrate03002(db *gorm.DB) error { + version := "0.3.0(2)" + return db.Transaction(func(tx *gorm.DB) error { + if err := addColumnIfNotExists(tx, &application.ApplicationConfig{}, "kr_website_id"); err != nil { + logger.Errorw("Migrate 03002 failed", logger.Field("action", "application.Config table add kr_website_id column"), logger.Field("error", err.Error())) + return err + } + return updateSystemVersion(tx, version) + }) +} diff --git a/initialize/migrate/tool.go b/initialize/migrate/tool.go new file mode 100644 index 0000000..8ce68c5 --- /dev/null +++ b/initialize/migrate/tool.go @@ -0,0 +1,101 @@ +package migrate + +import ( + "fmt" + "strings" + + "gorm.io/gorm" +) + +// ExecuteSQLFile 执行嵌入的 SQL 文件,去除注释 +func ExecuteSQLFile(tx *gorm.DB, path string) error { + // 读取 SQL 文件内容 + sqlContent, err := sqlFiles.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read embedded SQL file: %v", err) + } + + // 清除注释内容 + cleanedSQL := removeComments(string(sqlContent)) + + // 将清除注释后的内容按分号分割成多个 SQL 语句 + sqlStatements := splitSQLStatements(cleanedSQL) + + // 遍历 SQL 语句并执行 + for _, stmt := range sqlStatements { + stmt = strings.TrimSpace(stmt) + if stmt == "" { + continue + } + // 执行 SQL 语句 + if err := tx.Exec(stmt).Error; err != nil { + return fmt.Errorf("failed to execute SQL statement: %v \nSQL: %s", err.Error(), stmt) + } + } + return nil +} + +// removeComments 去除 SQL 代码中的注释 +func removeComments(sql string) string { + var result strings.Builder + inSingleLineComment := false + inMultiLineComment := false + length := len(sql) + + for i := 0; i < length; i++ { + // 处理 -- 单行注释 + if !inMultiLineComment && !inSingleLineComment && i+1 < length && sql[i] == '-' && sql[i+1] == '-' { + inSingleLineComment = true + i++ // 跳过 '-' + continue + } + + // 结束单行注释(支持 \r\n 和 \n) + if inSingleLineComment && (sql[i] == '\n' || sql[i] == '\r') { + inSingleLineComment = false + } + + // 处理 /* 多行注释 */ + if !inSingleLineComment && !inMultiLineComment && i+1 < length && sql[i] == '/' && sql[i+1] == '*' { + inMultiLineComment = true + i++ // 跳过 '*' + continue + } + + // 结束多行注释 + if inMultiLineComment && i+1 < length && sql[i] == '*' && sql[i+1] == '/' { + inMultiLineComment = false + i++ // 跳过 '/' + continue + } + + // 不是注释内容时,将字符添加到结果中 + if !inSingleLineComment && !inMultiLineComment { + result.WriteByte(sql[i]) + } + } + + // 去除多余的空白行 + lines := strings.Split(result.String(), "\n") + var cleanedLines []string + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed != "" { + cleanedLines = append(cleanedLines, trimmed) + } + } + return strings.Join(cleanedLines, "\n") +} + +// splitSQLStatements 更安全地分割 SQL 语句 +func splitSQLStatements(sql string) []string { + statements := strings.Split(sql, ";") + var results []string + for _, stmt := range statements { + trimmed := strings.TrimSpace(stmt) + if trimmed != "" { + results = append(results, trimmed) + } + } + return results +} diff --git a/initialize/mobile.go b/initialize/mobile.go new file mode 100644 index 0000000..696a725 --- /dev/null +++ b/initialize/mobile.go @@ -0,0 +1,32 @@ +package initialize + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/model/auth" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/tool" +) + +func Mobile(ctx *svc.ServiceContext) { + logger.Debug("Mobile config initialization") + method, err := ctx.AuthModel.FindOneByMethod(context.Background(), "mobile") + if err != nil { + panic(err) + } + var cfg config.MobileConfig + var mobileConfig auth.MobileAuthConfig + if err := mobileConfig.Unmarshal(method.Config); err != nil { + panic(fmt.Sprintf("failed to unmarshal mobile auth config: %v", err.Error())) + } + tool.DeepCopy(&cfg, mobileConfig) + cfg.Enable = *method.Enabled + value, _ := json.Marshal(mobileConfig.PlatformConfig) + cfg.PlatformConfig = string(value) + ctx.Config.Mobile = cfg +} diff --git a/initialize/mysql.go b/initialize/mysql.go new file mode 100644 index 0000000..17a45e2 --- /dev/null +++ b/initialize/mysql.go @@ -0,0 +1,10 @@ +package initialize + +import ( + "github.com/perfect-panel/ppanel-server/initialize/migrate" + "github.com/perfect-panel/ppanel-server/internal/svc" +) + +func Mysql(ctx *svc.ServiceContext) { + migrate.Migrate(ctx) +} diff --git a/initialize/node.go b/initialize/node.go new file mode 100644 index 0000000..a8d65bb --- /dev/null +++ b/initialize/node.go @@ -0,0 +1,51 @@ +package initialize + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/model/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/nodeMultiplier" + "github.com/perfect-panel/ppanel-server/pkg/tool" +) + +func Node(ctx *svc.ServiceContext) { + logger.Debug("Node config initialization") + configs, err := ctx.SystemModel.GetNodeConfig(context.Background()) + if err != nil { + panic(err) + } + var nodeConfig config.NodeConfig + tool.SystemConfigSliceReflectToStruct(configs, &nodeConfig) + ctx.Config.Node = nodeConfig + + // Manager initialization + if ctx.DB.Model(&system.System{}).Where("`key` = ?", "NodeMultiplierConfig").Find(&system.System{}).RowsAffected == 0 { + if err := ctx.DB.Model(&system.System{}).Create(&system.System{ + Key: "NodeMultiplierConfig", + Value: "[]", + Type: "string", + Desc: "Node Multiplier Config", + Category: "server", + }).Error; err != nil { + logger.Errorf("Create Node Multiplier Config Error: %s", err.Error()) + } + return + } + + nodeMultiplierData, err := ctx.SystemModel.FindNodeMultiplierConfig(context.Background()) + if err != nil { + + logger.Error("Get Node Multiplier Config Error: ", logger.Field("error", err.Error())) + return + } + var periods []nodeMultiplier.TimePeriod + if err := json.Unmarshal([]byte(nodeMultiplierData.Value), &periods); err != nil { + logger.Error("Unmarshal Node Multiplier Config Error: ", logger.Field("error", err.Error()), logger.Field("value", nodeMultiplierData.Value)) + } + ctx.NodeMultiplierManager = nodeMultiplier.NewNodeMultiplierManager(periods) +} diff --git a/initialize/oauth.go b/initialize/oauth.go new file mode 100644 index 0000000..70676ad --- /dev/null +++ b/initialize/oauth.go @@ -0,0 +1,11 @@ +package initialize + +import ( + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +func OAuth(svc *svc.ServiceContext) { + logger.Debug("OAuth config initialization") + +} diff --git a/initialize/register.go b/initialize/register.go new file mode 100644 index 0000000..d6ce119 --- /dev/null +++ b/initialize/register.go @@ -0,0 +1,23 @@ +package initialize + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/tool" +) + +func Register(ctx *svc.ServiceContext) { + logger.Debug("Register config initialization") + configs, err := ctx.SystemModel.GetRegisterConfig(context.Background()) + if err != nil { + logger.Errorf("[Init Register Config] Get Register Config Error: %s", err.Error()) + return + } + var registerConfig config.RegisterConfig + tool.SystemConfigSliceReflectToStruct(configs, ®isterConfig) + ctx.Config.Register = registerConfig +} diff --git a/initialize/site.go b/initialize/site.go new file mode 100644 index 0000000..20c82ab --- /dev/null +++ b/initialize/site.go @@ -0,0 +1,22 @@ +package initialize + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/tool" +) + +func Site(ctx *svc.ServiceContext) { + logger.Debug("initialize site config") + configs, err := ctx.SystemModel.GetSiteConfig(context.Background()) + if err != nil { + panic(err) + } + var siteConfig config.SiteConfig + tool.SystemConfigSliceReflectToStruct(configs, &siteConfig) + ctx.Config.Site = siteConfig +} diff --git a/initialize/statistics.go b/initialize/statistics.go new file mode 100644 index 0000000..fcb81c7 --- /dev/null +++ b/initialize/statistics.go @@ -0,0 +1,57 @@ +package initialize + +import ( + "context" + "time" + + "github.com/perfect-panel/ppanel-server/internal/model/cache" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +func TrafficDataToRedis(svcCtx *svc.ServiceContext) { + ctx := context.Background() + // 统计昨天的节点流量数据排行榜前10 + nodeData, err := svcCtx.TrafficLogModel.TopServersTrafficByDay(ctx, time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day()-1, 0, 0, 0, 0, time.Local), 10) + if err != nil { + logger.Errorw("统计昨天的流量数据失败", logger.Field("error", err.Error())) + } + var nodeCacheData []cache.NodeTodayTrafficRank + for _, node := range nodeData { + serverInfo, err := svcCtx.ServerModel.FindOne(ctx, node.ServerId) + if err != nil { + logger.Errorw("查询节点信息失败", logger.Field("error", err.Error())) + continue + } + nodeCacheData = append(nodeCacheData, cache.NodeTodayTrafficRank{ + ID: node.ServerId, + Name: serverInfo.Name, + Upload: node.Upload, + Download: node.Download, + Total: node.Upload + node.Download, + }) + } + // 写入缓存 + if err = svcCtx.NodeCache.UpdateYesterdayNodeTotalTrafficRank(ctx, nodeCacheData); err != nil { + logger.Errorw("写入昨天的流量数据到缓存失败", logger.Field("error", err.Error())) + } + // 统计昨天的用户流量数据排行榜前10 + userData, err := svcCtx.TrafficLogModel.TopUsersTrafficByDay(ctx, time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day()-1, 0, 0, 0, 0, time.Local), 10) + if err != nil { + logger.Errorw("统计昨天的流量数据失败", logger.Field("error", err.Error())) + } + var userCacheData []cache.UserTodayTrafficRank + for _, user := range userData { + userCacheData = append(userCacheData, cache.UserTodayTrafficRank{ + SID: user.SubscribeId, + Upload: user.Upload, + Download: user.Download, + Total: user.Upload + user.Download, + }) + } + // 写入缓存 + if err = svcCtx.NodeCache.UpdateYesterdayUserTotalTrafficRank(ctx, userCacheData); err != nil { + logger.Errorw("写入昨天的流量数据到缓存失败", logger.Field("error", err.Error())) + } + logger.Infow("初始化昨天的流量数据到缓存成功") +} diff --git a/initialize/subscribe.go b/initialize/subscribe.go new file mode 100644 index 0000000..a584a3d --- /dev/null +++ b/initialize/subscribe.go @@ -0,0 +1,24 @@ +package initialize + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/tool" +) + +func Subscribe(svc *svc.ServiceContext) { + logger.Debug("Subscribe config initialization") + configs, err := svc.SystemModel.GetSubscribeConfig(context.Background()) + if err != nil { + logger.Error("[Init Subscribe Config] Get Subscribe Config Error: ", logger.Field("error", err.Error())) + return + } + + var subscribeConfig config.SubscribeConfig + tool.SystemConfigSliceReflectToStruct(configs, &subscribeConfig) + svc.Config.Subscribe = subscribeConfig +} diff --git a/initialize/telegram.go b/initialize/telegram.go new file mode 100644 index 0000000..3a2e3a8 --- /dev/null +++ b/initialize/telegram.go @@ -0,0 +1,82 @@ +package initialize + +import ( + "context" + "fmt" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/logic/telegram" + "github.com/perfect-panel/ppanel-server/internal/model/auth" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/tool" +) + +func Telegram(svc *svc.ServiceContext) { + + method, err := svc.AuthModel.FindOneByMethod(context.Background(), "telegram") + if err != nil { + logger.Errorf("[Init Telegram Config] Get Telegram Config Error: %s", err.Error()) + return + } + var tg config.Telegram + + tgConfig := new(auth.TelegramAuthConfig) + if err = tgConfig.Unmarshal(method.Config); err != nil { + logger.Errorf("[Init Telegram Config] Unmarshal Telegram Config Error: %s", err.Error()) + return + } + + if tgConfig.BotToken == "" { + logger.Debug("[Init Telegram Config] Telegram Token is empty") + return + } + + bot, err := tgbotapi.NewBotAPI(tg.BotToken) + if err != nil { + logger.Error("[Init Telegram Config] New Bot API Error: ", logger.Field("error", err.Error())) + return + } + + if tgConfig.WebHookDomain == "" || svc.Config.Debug { + // set Long Polling mode + updateConfig := tgbotapi.NewUpdate(0) + updateConfig.Timeout = 60 + updates := bot.GetUpdatesChan(updateConfig) + go func() { + for update := range updates { + if update.Message != nil { + ctx := context.Background() + l := telegram.NewTelegramLogic(ctx, svc) + l.TelegramLogic(&update) + } + } + }() + } else { + wh, err := tgbotapi.NewWebhook(fmt.Sprintf("%s/v1/telegram/webhook?secret=%s", tgConfig.WebHookDomain, tool.Md5Encode(tgConfig.BotToken, false))) + if err != nil { + logger.Errorf("[Init Telegram Config] New Webhook Error: %s", err.Error()) + return + } + _, err = bot.Request(wh) + if err != nil { + logger.Errorf("[Init Telegram Config] Request Webhook Error: %s", err.Error()) + return + } + } + + user, err := bot.GetMe() + if err != nil { + logger.Error("[Init Telegram Config] Get Bot Info Error: ", logger.Field("error", err.Error())) + return + } + svc.Config.Telegram.BotID = user.ID + svc.Config.Telegram.BotName = user.UserName + svc.Config.Telegram.EnableNotify = tg.EnableNotify + svc.Config.Telegram.WebHookDomain = tg.WebHookDomain + svc.TelegramBot = bot + + logger.Info("[Init Telegram Config] Webhook set success") +} diff --git a/initialize/templates/index.html b/initialize/templates/index.html new file mode 100644 index 0000000..ba06f13 --- /dev/null +++ b/initialize/templates/index.html @@ -0,0 +1,469 @@ + + + + + + + PPanel - Application Initialization + + + + + + + + +
+
+ +
+
+

Welcome to PPanel Setup

+ +
+

Let's get your PPanel application up and running. Please provide the necessary information below.

+ +
+ +
+

Administrator Details

+
+
+ + + +
+
+ + + +
+
+
+ + +
+
+

MySQL Database Setup

+ +
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + +
+
+ + + +
+
+
+ + +
+
+

Redis Cache Setup

+ +
+
+
+ + + +
+
+ + + +
+
+ + +
+
+
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + diff --git a/initialize/verify.go b/initialize/verify.go new file mode 100644 index 0000000..c7a2f1a --- /dev/null +++ b/initialize/verify.go @@ -0,0 +1,48 @@ +package initialize + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/tool" +) + +type verifyConfig struct { + TurnstileSiteKey string + TurnstileSecret string + EnableLoginVerify bool + EnableRegisterVerify bool + EnableResetPasswordVerify bool +} + +func Verify(svc *svc.ServiceContext) { + logger.Debug("Verify config initialization") + configs, err := svc.SystemModel.GetVerifyConfig(context.Background()) + if err != nil { + logger.Error("[Init Verify Config] Get Verify Config Error: ", logger.Field("error", err.Error())) + return + } + var verify verifyConfig + tool.SystemConfigSliceReflectToStruct(configs, &verify) + svc.Config.Verify = config.Verify{ + TurnstileSiteKey: verify.TurnstileSiteKey, + TurnstileSecret: verify.TurnstileSecret, + LoginVerify: verify.EnableLoginVerify, + RegisterVerify: verify.EnableRegisterVerify, + ResetPasswordVerify: verify.EnableResetPasswordVerify, + } + + logger.Debug("Verify code config initialization") + + var verifyCodeConfig config.VerifyCode + cfg, err := svc.SystemModel.GetVerifyCodeConfig(context.Background()) + if err != nil { + logger.Errorf("[Init Verify Config] Get Verify Code Config Error: %s", err.Error()) + return + } + tool.SystemConfigSliceReflectToStruct(cfg, &verifyCodeConfig) + svc.Config.VerifyCode = verifyCodeConfig +} diff --git a/initialize/version.go b/initialize/version.go new file mode 100644 index 0000000..fbabfac --- /dev/null +++ b/initialize/version.go @@ -0,0 +1,127 @@ +package initialize + +import ( + "fmt" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/initialize/migrate/patch" + "github.com/perfect-panel/ppanel-server/internal/model/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/tool" +) + +func VerifyVersion(ctx *svc.ServiceContext) { + var configVersion system.System + err := ctx.DB.Transaction(func(db *gorm.DB) error { + db.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").First(&configVersion) + if configVersion.Value != constant.Version { + // Version eg: 1.0.0(10000) + current := tool.ExtractVersionNumber(constant.Version) + sqlVersion := tool.ExtractVersionNumber(configVersion.Value) + logger.Infof("Verify System Version, current version: %d, datebase version: %d", current, sqlVersion) + if current > sqlVersion { + // Migrate to Milestone Version + // + // Migrate SQL to 0.1.7(01703) + if sqlVersion < 1705 { + if err := migrate01701(db, sqlVersion); err != nil { + return err + } + } + // 重新执行2000版本的迁移 + if sqlVersion == 2002 { + sqlVersion = 2000 + } + // Migrate SQL to 0.2.0(02000) + if sqlVersion < 2009 { + if err := migrate02000(db, sqlVersion); err != nil { + return err + } + } + // Migrate SQL to 0.3.0(03000) + if sqlVersion < 3002 { + if err := migrate03000(db, sqlVersion); err != nil { + return err + } + } + + } + } + return nil + }) + if err != nil { + panic("update system version error:" + err.Error()) + } +} +func migrate01701(db *gorm.DB, sqlVersion int) error { + migrations := map[int]func(*gorm.DB) error{ + 1200: patch.Migrate01200, + 1201: patch.Migrate01201, + 1202: patch.Migrate01202, + 1203: patch.Migrate01203, + 1204: patch.Migrate01204, + 1205: patch.Migrate01205, + 1301: patch.Migrate01301, + 1602: patch.Migrate01602, + 1701: patch.Migrate01701, + 1702: patch.Migrate01702, + 1703: patch.Migrate01703, + 1704: patch.Migrate01704, + 1705: patch.Migrate01705, + } + + for v, migrate := range migrations { + if sqlVersion < v { + if err := migrate(db); err != nil { + return fmt.Errorf("migrator %d version error: %w", v, err) + } + logger.Infof(fmt.Sprintf("Migrate %d version success", v)) + } + } + return nil +} + +func migrate02000(db *gorm.DB, sqlVersion int) error { + migrations := map[int]func(*gorm.DB) error{ + 2000: patch.Migrate02000, + 2001: patch.Migrate02001, + 2002: patch.Migrate02002, + 2003: patch.Migrate02003, + 2007: patch.Migrate02007, + 2008: patch.Migrate02008, + 2009: patch.Migrate02009, + 2010: patch.Migrate02010, + 2011: patch.Migrate02011, + } + + for v, migrate := range migrations { + if sqlVersion < v { + if err := migrate(db); err != nil { + return fmt.Errorf("migrator %d version error: %w", v, err) + } + logger.Infof(fmt.Sprintf("Migrate %d version success", v)) + } + } + return nil +} + +func migrate03000(db *gorm.DB, sqlVersion int) error { + migrations := map[int]func(*gorm.DB) error{ + 3001: patch.Migrate03001, + 3002: patch.Migrate03002, + } + + for v, migrate := range migrations { + if sqlVersion < v { + if err := migrate(db); err != nil { + return fmt.Errorf("migrator %d version error: %w", v, err) + } + logger.Infof(fmt.Sprintf("Migrate %d version success", v)) + } + } + return nil +} diff --git a/internal/config/cacheKey.go b/internal/config/cacheKey.go new file mode 100644 index 0000000..02f5be9 --- /dev/null +++ b/internal/config/cacheKey.go @@ -0,0 +1,78 @@ +package config + +// CurrencyConfigKey Currency Config Key +const CurrencyConfigKey = "system:currency_config" + +// SmsConfigKey Mobile Config Key +const SmsConfigKey = "system:sms_config" + +// SiteConfigKey Site Config Key +const SiteConfigKey = "system:site_config" + +// SubscribeConfigKey Subscribe Config Key +const SubscribeConfigKey = "system:subscribe_config" + +// ApplicationKey Application Key +const ApplicationKey = "system:application" + +// RegisterConfigKey Register Config Key +const RegisterConfigKey = "system:register_config" + +// VerifyConfigKey Verify Config Key +const VerifyConfigKey = "system:verify_config" + +// EmailSmtpConfigKey Email Smtp Config Key +const EmailSmtpConfigKey = "system:email_smtp_config" + +// NodeConfigKey Node Config Key +const NodeConfigKey = "system:node_config" + +// InviteConfigKey Invite Config Key +const InviteConfigKey = "system:invite_config" + +// TelegramConfigKey Telegram Config Key +const TelegramConfigKey = "system:telegram_config" + +// TosConfigKey Tos配置 +const TosConfigKey = "system:tos_config" + +// VerifyCodeConfigKey Verify Code Config Key +const VerifyCodeConfigKey = "system:verify_code_config" + +// SessionIdKey cache session key +const SessionIdKey = "auth:session_id" + +// GlobalConfigKey Global Config Key +const GlobalConfigKey = "system:global_config" + +// AuthCodeCacheKey Register Code Cache Key +const AuthCodeCacheKey = "auth:verify:email" + +// AuthCodeTelephoneCacheKey Register Code Cache Key +const AuthCodeTelephoneCacheKey = "auth:verify:telephone" + +// ServerUserListCacheKey Server User List Cache Key +const ServerUserListCacheKey = "server:user_list:id:" + +// ServerConfigCacheKey Server Config Cache Key +const ServerConfigCacheKey = "server:config:id:" + +// CommonStat Cache Key +const CommonStatCacheKey = "common:stat" + +// ServerStatusCacheKey Server Status Cache Key +const ServerStatusCacheKey = "server:status:id:" + +// ServerCountCacheKey Server Count Cache Key +const ServerCountCacheKey = "server:count" + +// UserBindTelegramCacheKey User Bind Telegram Cache Key +const UserBindTelegramCacheKey = "user:bind:telegram:code:" + +const CacheSmsCount = "cache:sms:count" + +// SendIntervalKeyPrefix Auth Code Send Interval Key Prefix +const SendIntervalKeyPrefix = "send:interval:" + +// SendCountLimitKeyPrefix Send Count Limit Key Prefix eg. send:limit:register:email:xxx@ppanel.dev +const SendCountLimitKeyPrefix = "send:limit:" diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..824d656 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,146 @@ +package config + +import ( + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/orm" +) + +type Config struct { + Model string `yaml:"Model" default:"prod"` + Host string `yaml:"Host" default:"0.0.0.0"` + Port int `yaml:"Port" default:"8080"` + Debug bool `yaml:"Debug" default:"false"` + TLS TLS `yaml:"TLS"` + JwtAuth JwtAuth `yaml:"JwtAuth"` + Logger logger.LogConf `yaml:"Logger"` + MySQL orm.Config `yaml:"MySQL"` + Redis RedisConfig `yaml:"Redis"` + Site SiteConfig `yaml:"Site"` + Node NodeConfig `yaml:"Node"` + Mobile MobileConfig `yaml:"Mobile"` + Email EmailConfig `yaml:"Email"` + Verify Verify `yaml:"Verify"` + VerifyCode VerifyCode `yaml:"VerifyCode"` + Register RegisterConfig `yaml:"Register"` + Subscribe SubscribeConfig `yaml:"Subscribe"` + Invite InviteConfig `yaml:"Invite"` + Telegram Telegram `yaml:"Telegram"` + Administrator struct { + Email string `yaml:"Email" default:"admin@ppanel.dev"` + Password string `yaml:"Password" default:"password"` + } `yaml:"Administrator"` +} + +type RedisConfig struct { + Host string `yaml:"Host" default:"localhost:6379"` + Pass string `yaml:"Pass" default:""` + DB int `yaml:"DB" default:"0"` +} + +type JwtAuth struct { + AccessSecret string `yaml:"AccessSecret"` + AccessExpire int64 `yaml:"AccessExpire" default:"604800"` +} + +type Verify struct { + TurnstileSiteKey string `yaml:"TurnstileSiteKey" default:""` + TurnstileSecret string `yaml:"TurnstileSecret" default:""` + LoginVerify bool `yaml:"LoginVerify" default:"false"` + RegisterVerify bool `yaml:"RegisterVerify" default:"false"` + ResetPasswordVerify bool `yaml:"ResetPasswordVerify" default:"false"` +} + +type SubscribeConfig struct { + SingleModel bool `yaml:"SingleModel" default:"false"` + SubscribePath string `yaml:"SubscribePath" default:"/api/subscribe"` + SubscribeDomain string `yaml:"SubscribeDomain" default:""` + PanDomain bool `yaml:"PanDomain" default:"false"` +} + +type RegisterConfig struct { + StopRegister bool `yaml:"StopRegister" default:"false"` + EnableTrial bool `yaml:"EnableTrial" default:"false"` + TrialSubscribe int64 `yaml:"TrialSubscribe" default:"0"` + TrialTime int64 `yaml:"TrialTime" default:"0"` + TrialTimeUnit string `yaml:"TrialTimeUnit" default:""` + IpRegisterLimit int64 `yaml:"IpRegisterLimit" default:"0"` + IpRegisterLimitDuration int64 `yaml:"IpRegisterLimitDuration" default:"0"` + EnableIpRegisterLimit bool `yaml:"EnableIpRegisterLimit" default:"false"` +} + +type EmailConfig struct { + Enable bool `yaml:"Enable" default:"true"` + Platform string `yaml:"platform"` + PlatformConfig string `yaml:"platform_config"` + EnableVerify bool `yaml:"enable_verify"` + EnableNotify bool `yaml:"enable_notify"` + EnableDomainSuffix bool `yaml:"enable_domain_suffix"` + DomainSuffixList string `yaml:"domain_suffix_list"` + VerifyEmailTemplate string `yaml:"verify_email_template"` + ExpirationEmailTemplate string `yaml:"expiration_email_template"` + MaintenanceEmailTemplate string `yaml:"maintenance_email_template"` + TrafficExceedEmailTemplate string `yaml:"traffic_exceed_email_template"` +} + +type MobileConfig struct { + Enable bool `yaml:"Enable" default:"true"` + Platform string `yaml:"platform"` + PlatformConfig string `yaml:"platform_config"` + EnableVerify bool `yaml:"enable_verify"` + EnableWhitelist bool `yaml:"enable_whitelist"` + Whitelist []string `yaml:"whitelist"` +} + +type SiteConfig struct { + Host string `yaml:"Host" default:""` + SiteName string `yaml:"SiteName" default:""` + SiteDesc string `yaml:"SiteDesc" default:""` + SiteLogo string `yaml:"SiteLogo" default:""` + Keywords string `yaml:"Keywords" default:""` + CustomHTML string `yaml:"CustomHTML" default:""` + CustomData string `yaml:"CustomData" default:""` +} + +type NodeConfig struct { + NodeSecret string `yaml:"NodeSecret" default:""` + NodePullInterval int64 `yaml:"NodePullInterval" default:"60"` + NodePushInterval int64 `yaml:"NodePushInterval" default:"60"` +} + +type File struct { + Host string `yaml:"Host" default:"0.0.0.0"` + Port int `yaml:"Port" default:"8080"` + TLS TLS `yaml:"TLS"` + Debug bool `yaml:"Debug" default:"true"` + JwtAuth JwtAuth `yaml:"JwtAuth"` + Logger logger.LogConf `yaml:"Logger"` + MySQL orm.Config `yaml:"MySQL"` + Redis RedisConfig `yaml:"Redis"` +} + +type InviteConfig struct { + ForcedInvite bool `yaml:"ForcedInvite" default:"false"` + ReferralPercentage int64 `yaml:"ReferralPercentage" default:"0"` + OnlyFirstPurchase bool `yaml:"OnlyFirstPurchase" default:"false"` +} + +type Telegram struct { + Enable bool `yaml:"Enable" default:"false"` + BotID int64 `yaml:"BotID" default:""` + BotName string `yaml:"BotName" default:""` + BotToken string `yaml:"BotToken" default:""` + EnableNotify bool `yaml:"EnableNotify" default:"false"` + WebHookDomain string `yaml:"WebHookDomain" default:""` +} + +type TLS struct { + Enable bool `yaml:"Enable" default:"false"` + CertFile string `yaml:"CertFile" default:""` + KeyFile string `yaml:"KeyFile" default:""` +} + +type VerifyCode struct { + ExpireTime int64 `yaml:"ExpireTime" default:"300"` + Limit int64 `yaml:"Limit" default:"15"` + Interval int64 `yaml:"Interval" default:"60"` +} diff --git a/internal/config/constant.go b/internal/config/constant.go new file mode 100644 index 0000000..3f7625e --- /dev/null +++ b/internal/config/constant.go @@ -0,0 +1,5 @@ +package config + +import "github.com/perfect-panel/ppanel-server/pkg/constant" + +const Version = constant.Version diff --git a/internal/config/protocol.go b/internal/config/protocol.go new file mode 100644 index 0000000..d334f58 --- /dev/null +++ b/internal/config/protocol.go @@ -0,0 +1,10 @@ +package config + +type Protocol string + +const ( + Shadowsocks Protocol = "shadowsocks" + Trojan Protocol = "trojan" + Vmess Protocol = "vmess" + Vless Protocol = "vless" +) diff --git a/internal/handler/admin/ads/createAdsHandler.go b/internal/handler/admin/ads/createAdsHandler.go new file mode 100644 index 0000000..d06a5ed --- /dev/null +++ b/internal/handler/admin/ads/createAdsHandler.go @@ -0,0 +1,27 @@ +package ads + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/ads" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Create Ads +func CreateAdsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateAdsRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + ctx := c.Request.Context() + l := ads.NewCreateAdsLogic(ctx, svcCtx) + err := l.CreateAds(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/ads/deleteAdsHandler.go b/internal/handler/admin/ads/deleteAdsHandler.go new file mode 100644 index 0000000..efb153d --- /dev/null +++ b/internal/handler/admin/ads/deleteAdsHandler.go @@ -0,0 +1,27 @@ +package ads + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/ads" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Delete Ads +func DeleteAdsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeleteAdsRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + ctx := c.Request.Context() + l := ads.NewDeleteAdsLogic(ctx, svcCtx) + err := l.DeleteAds(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/ads/getAdsDetailHandler.go b/internal/handler/admin/ads/getAdsDetailHandler.go new file mode 100644 index 0000000..3c3eb4d --- /dev/null +++ b/internal/handler/admin/ads/getAdsDetailHandler.go @@ -0,0 +1,27 @@ +package ads + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/ads" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get Ads Detail +func GetAdsDetailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetAdsDetailRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + ctx := c.Request.Context() + l := ads.NewGetAdsDetailLogic(ctx, svcCtx) + resp, err := l.GetAdsDetail(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/ads/getAdsListHandler.go b/internal/handler/admin/ads/getAdsListHandler.go new file mode 100644 index 0000000..e2dc1ec --- /dev/null +++ b/internal/handler/admin/ads/getAdsListHandler.go @@ -0,0 +1,27 @@ +package ads + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/ads" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get Ads List +func GetAdsListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetAdsListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + ctx := c.Request.Context() + l := ads.NewGetAdsListLogic(ctx, svcCtx) + resp, err := l.GetAdsList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/ads/updateAdsHandler.go b/internal/handler/admin/ads/updateAdsHandler.go new file mode 100644 index 0000000..801365b --- /dev/null +++ b/internal/handler/admin/ads/updateAdsHandler.go @@ -0,0 +1,27 @@ +package ads + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/ads" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update Ads +func UpdateAdsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateAdsRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + ctx := c.Request.Context() + l := ads.NewUpdateAdsLogic(ctx, svcCtx) + err := l.UpdateAds(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/announcement/createAnnouncementHandler.go b/internal/handler/admin/announcement/createAnnouncementHandler.go new file mode 100644 index 0000000..138b23c --- /dev/null +++ b/internal/handler/admin/announcement/createAnnouncementHandler.go @@ -0,0 +1,26 @@ +package announcement + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/announcement" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Create announcement +func CreateAnnouncementHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateAnnouncementRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := announcement.NewCreateAnnouncementLogic(c.Request.Context(), svcCtx) + err := l.CreateAnnouncement(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/announcement/deleteAnnouncementHandler.go b/internal/handler/admin/announcement/deleteAnnouncementHandler.go new file mode 100644 index 0000000..ae673d0 --- /dev/null +++ b/internal/handler/admin/announcement/deleteAnnouncementHandler.go @@ -0,0 +1,26 @@ +package announcement + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/announcement" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Delete announcement +func DeleteAnnouncementHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeleteAnnouncementRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := announcement.NewDeleteAnnouncementLogic(c.Request.Context(), svcCtx) + err := l.DeleteAnnouncement(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/announcement/getAnnouncementHandler.go b/internal/handler/admin/announcement/getAnnouncementHandler.go new file mode 100644 index 0000000..ea51c64 --- /dev/null +++ b/internal/handler/admin/announcement/getAnnouncementHandler.go @@ -0,0 +1,26 @@ +package announcement + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/announcement" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get announcement +func GetAnnouncementHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetAnnouncementRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := announcement.NewGetAnnouncementLogic(c.Request.Context(), svcCtx) + resp, err := l.GetAnnouncement(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/announcement/getAnnouncementListHandler.go b/internal/handler/admin/announcement/getAnnouncementListHandler.go new file mode 100644 index 0000000..b5c0f55 --- /dev/null +++ b/internal/handler/admin/announcement/getAnnouncementListHandler.go @@ -0,0 +1,26 @@ +package announcement + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/announcement" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get announcement list +func GetAnnouncementListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetAnnouncementListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := announcement.NewGetAnnouncementListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetAnnouncementList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/announcement/updateAnnouncementHandler.go b/internal/handler/admin/announcement/updateAnnouncementHandler.go new file mode 100644 index 0000000..96be68c --- /dev/null +++ b/internal/handler/admin/announcement/updateAnnouncementHandler.go @@ -0,0 +1,26 @@ +package announcement + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/announcement" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update announcement +func UpdateAnnouncementHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateAnnouncementRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := announcement.NewUpdateAnnouncementLogic(c.Request.Context(), svcCtx) + err := l.UpdateAnnouncement(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/authMethod/getAuthMethodConfigHandler.go b/internal/handler/admin/authMethod/getAuthMethodConfigHandler.go new file mode 100644 index 0000000..ac94fc1 --- /dev/null +++ b/internal/handler/admin/authMethod/getAuthMethodConfigHandler.go @@ -0,0 +1,26 @@ +package authMethod + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/authMethod" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get auth method config +func GetAuthMethodConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetAuthMethodConfigRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := authMethod.NewGetAuthMethodConfigLogic(c.Request.Context(), svcCtx) + resp, err := l.GetAuthMethodConfig(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/authMethod/getAuthMethodListHandler.go b/internal/handler/admin/authMethod/getAuthMethodListHandler.go new file mode 100644 index 0000000..9e20e2f --- /dev/null +++ b/internal/handler/admin/authMethod/getAuthMethodListHandler.go @@ -0,0 +1,18 @@ +package authMethod + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/authMethod" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get auth method list +func GetAuthMethodListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := authMethod.NewGetAuthMethodListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetAuthMethodList() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/authMethod/getEmailPlatformHandler.go b/internal/handler/admin/authMethod/getEmailPlatformHandler.go new file mode 100644 index 0000000..97a2aba --- /dev/null +++ b/internal/handler/admin/authMethod/getEmailPlatformHandler.go @@ -0,0 +1,18 @@ +package authMethod + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/authMethod" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get email support platform +func GetEmailPlatformHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := authMethod.NewGetEmailPlatformLogic(c.Request.Context(), svcCtx) + resp, err := l.GetEmailPlatform() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/authMethod/getSmsPlatformHandler.go b/internal/handler/admin/authMethod/getSmsPlatformHandler.go new file mode 100644 index 0000000..15dcd91 --- /dev/null +++ b/internal/handler/admin/authMethod/getSmsPlatformHandler.go @@ -0,0 +1,18 @@ +package authMethod + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/authMethod" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get sms support platform +func GetSmsPlatformHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := authMethod.NewGetSmsPlatformLogic(c.Request.Context(), svcCtx) + resp, err := l.GetSmsPlatform() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/authMethod/testEmailSendHandler.go b/internal/handler/admin/authMethod/testEmailSendHandler.go new file mode 100644 index 0000000..fd98afd --- /dev/null +++ b/internal/handler/admin/authMethod/testEmailSendHandler.go @@ -0,0 +1,26 @@ +package authMethod + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/authMethod" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Test email send +func TestEmailSendHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.TestEmailSendRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := authMethod.NewTestEmailSendLogic(c.Request.Context(), svcCtx) + err := l.TestEmailSend(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/authMethod/testSmsSendHandler.go b/internal/handler/admin/authMethod/testSmsSendHandler.go new file mode 100644 index 0000000..2bc551e --- /dev/null +++ b/internal/handler/admin/authMethod/testSmsSendHandler.go @@ -0,0 +1,26 @@ +package authMethod + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/authMethod" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Test sms send +func TestSmsSendHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.TestSmsSendRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := authMethod.NewTestSmsSendLogic(c.Request.Context(), svcCtx) + err := l.TestSmsSend(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/authMethod/updateAuthMethodConfigHandler.go b/internal/handler/admin/authMethod/updateAuthMethodConfigHandler.go new file mode 100644 index 0000000..0af525e --- /dev/null +++ b/internal/handler/admin/authMethod/updateAuthMethodConfigHandler.go @@ -0,0 +1,26 @@ +package authMethod + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/authMethod" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update auth method config +func UpdateAuthMethodConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateAuthMethodConfigRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := authMethod.NewUpdateAuthMethodConfigLogic(c.Request.Context(), svcCtx) + resp, err := l.UpdateAuthMethodConfig(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/console/queryRevenueStatisticsHandler.go b/internal/handler/admin/console/queryRevenueStatisticsHandler.go new file mode 100644 index 0000000..2894000 --- /dev/null +++ b/internal/handler/admin/console/queryRevenueStatisticsHandler.go @@ -0,0 +1,18 @@ +package console + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/console" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Query revenue statistics +func QueryRevenueStatisticsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := console.NewQueryRevenueStatisticsLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryRevenueStatistics() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/console/queryServerTotalDataHandler.go b/internal/handler/admin/console/queryServerTotalDataHandler.go new file mode 100644 index 0000000..d646cc6 --- /dev/null +++ b/internal/handler/admin/console/queryServerTotalDataHandler.go @@ -0,0 +1,18 @@ +package console + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/console" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Query server total data +func QueryServerTotalDataHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := console.NewQueryServerTotalDataLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryServerTotalData() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/console/queryTicketWaitReplyHandler.go b/internal/handler/admin/console/queryTicketWaitReplyHandler.go new file mode 100644 index 0000000..b39a61e --- /dev/null +++ b/internal/handler/admin/console/queryTicketWaitReplyHandler.go @@ -0,0 +1,18 @@ +package console + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/console" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Query ticket wait reply +func QueryTicketWaitReplyHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := console.NewQueryTicketWaitReplyLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryTicketWaitReply() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/console/queryUserStatisticsHandler.go b/internal/handler/admin/console/queryUserStatisticsHandler.go new file mode 100644 index 0000000..1d651b3 --- /dev/null +++ b/internal/handler/admin/console/queryUserStatisticsHandler.go @@ -0,0 +1,18 @@ +package console + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/console" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Query user statistics +func QueryUserStatisticsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := console.NewQueryUserStatisticsLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryUserStatistics() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/coupon/batchDeleteCouponHandler.go b/internal/handler/admin/coupon/batchDeleteCouponHandler.go new file mode 100644 index 0000000..cbb6b0c --- /dev/null +++ b/internal/handler/admin/coupon/batchDeleteCouponHandler.go @@ -0,0 +1,26 @@ +package coupon + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/coupon" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Batch delete coupon +func BatchDeleteCouponHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.BatchDeleteCouponRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := coupon.NewBatchDeleteCouponLogic(c.Request.Context(), svcCtx) + err := l.BatchDeleteCoupon(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/coupon/createCouponHandler.go b/internal/handler/admin/coupon/createCouponHandler.go new file mode 100644 index 0000000..baf9c7a --- /dev/null +++ b/internal/handler/admin/coupon/createCouponHandler.go @@ -0,0 +1,26 @@ +package coupon + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/coupon" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Create coupon +func CreateCouponHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateCouponRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := coupon.NewCreateCouponLogic(c.Request.Context(), svcCtx) + err := l.CreateCoupon(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/coupon/deleteCouponHandler.go b/internal/handler/admin/coupon/deleteCouponHandler.go new file mode 100644 index 0000000..651de28 --- /dev/null +++ b/internal/handler/admin/coupon/deleteCouponHandler.go @@ -0,0 +1,26 @@ +package coupon + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/coupon" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Delete coupon +func DeleteCouponHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeleteCouponRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := coupon.NewDeleteCouponLogic(c.Request.Context(), svcCtx) + err := l.DeleteCoupon(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/coupon/getCouponListHandler.go b/internal/handler/admin/coupon/getCouponListHandler.go new file mode 100644 index 0000000..ce0b1d0 --- /dev/null +++ b/internal/handler/admin/coupon/getCouponListHandler.go @@ -0,0 +1,26 @@ +package coupon + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/coupon" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get coupon list +func GetCouponListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetCouponListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := coupon.NewGetCouponListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetCouponList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/coupon/updateCouponHandler.go b/internal/handler/admin/coupon/updateCouponHandler.go new file mode 100644 index 0000000..3443553 --- /dev/null +++ b/internal/handler/admin/coupon/updateCouponHandler.go @@ -0,0 +1,26 @@ +package coupon + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/coupon" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update coupon +func UpdateCouponHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateCouponRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := coupon.NewUpdateCouponLogic(c.Request.Context(), svcCtx) + err := l.UpdateCoupon(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/document/batchDeleteDocumentHandler.go b/internal/handler/admin/document/batchDeleteDocumentHandler.go new file mode 100644 index 0000000..490125c --- /dev/null +++ b/internal/handler/admin/document/batchDeleteDocumentHandler.go @@ -0,0 +1,26 @@ +package document + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/document" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Batch delete document +func BatchDeleteDocumentHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.BatchDeleteDocumentRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := document.NewBatchDeleteDocumentLogic(c.Request.Context(), svcCtx) + err := l.BatchDeleteDocument(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/document/createDocumentHandler.go b/internal/handler/admin/document/createDocumentHandler.go new file mode 100644 index 0000000..08d0fcb --- /dev/null +++ b/internal/handler/admin/document/createDocumentHandler.go @@ -0,0 +1,26 @@ +package document + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/document" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Create document +func CreateDocumentHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateDocumentRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := document.NewCreateDocumentLogic(c.Request.Context(), svcCtx) + err := l.CreateDocument(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/document/deleteDocumentHandler.go b/internal/handler/admin/document/deleteDocumentHandler.go new file mode 100644 index 0000000..6dd5a7b --- /dev/null +++ b/internal/handler/admin/document/deleteDocumentHandler.go @@ -0,0 +1,26 @@ +package document + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/document" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Delete document +func DeleteDocumentHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeleteDocumentRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := document.NewDeleteDocumentLogic(c.Request.Context(), svcCtx) + err := l.DeleteDocument(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/document/getDocumentDetailHandler.go b/internal/handler/admin/document/getDocumentDetailHandler.go new file mode 100644 index 0000000..f272f12 --- /dev/null +++ b/internal/handler/admin/document/getDocumentDetailHandler.go @@ -0,0 +1,26 @@ +package document + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/document" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get document detail +func GetDocumentDetailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetDocumentDetailRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := document.NewGetDocumentDetailLogic(c.Request.Context(), svcCtx) + resp, err := l.GetDocumentDetail(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/document/getDocumentListHandler.go b/internal/handler/admin/document/getDocumentListHandler.go new file mode 100644 index 0000000..48a3e62 --- /dev/null +++ b/internal/handler/admin/document/getDocumentListHandler.go @@ -0,0 +1,26 @@ +package document + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/document" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get document list +func GetDocumentListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetDocumentListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := document.NewGetDocumentListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetDocumentList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/document/updateDocumentHandler.go b/internal/handler/admin/document/updateDocumentHandler.go new file mode 100644 index 0000000..b88798b --- /dev/null +++ b/internal/handler/admin/document/updateDocumentHandler.go @@ -0,0 +1,26 @@ +package document + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/document" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update document +func UpdateDocumentHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateDocumentRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := document.NewUpdateDocumentLogic(c.Request.Context(), svcCtx) + err := l.UpdateDocument(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/log/getMessageLogListHandler.go b/internal/handler/admin/log/getMessageLogListHandler.go new file mode 100644 index 0000000..ab535a2 --- /dev/null +++ b/internal/handler/admin/log/getMessageLogListHandler.go @@ -0,0 +1,26 @@ +package log + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/log" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get message log list +func GetMessageLogListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetMessageLogListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := log.NewGetMessageLogListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetMessageLogList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/order/createOrderHandler.go b/internal/handler/admin/order/createOrderHandler.go new file mode 100644 index 0000000..f48ba16 --- /dev/null +++ b/internal/handler/admin/order/createOrderHandler.go @@ -0,0 +1,26 @@ +package order + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/order" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Create order +func CreateOrderHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateOrderRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := order.NewCreateOrderLogic(c.Request.Context(), svcCtx) + err := l.CreateOrder(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/order/getOrderListHandler.go b/internal/handler/admin/order/getOrderListHandler.go new file mode 100644 index 0000000..9e732bb --- /dev/null +++ b/internal/handler/admin/order/getOrderListHandler.go @@ -0,0 +1,26 @@ +package order + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/order" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get order list +func GetOrderListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetOrderListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := order.NewGetOrderListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetOrderList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/order/updateOrderStatusHandler.go b/internal/handler/admin/order/updateOrderStatusHandler.go new file mode 100644 index 0000000..51a5c19 --- /dev/null +++ b/internal/handler/admin/order/updateOrderStatusHandler.go @@ -0,0 +1,26 @@ +package order + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/order" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update order status +func UpdateOrderStatusHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateOrderStatusRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := order.NewUpdateOrderStatusLogic(c.Request.Context(), svcCtx) + err := l.UpdateOrderStatus(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/payment/createPaymentMethodHandler.go b/internal/handler/admin/payment/createPaymentMethodHandler.go new file mode 100644 index 0000000..69961a7 --- /dev/null +++ b/internal/handler/admin/payment/createPaymentMethodHandler.go @@ -0,0 +1,26 @@ +package payment + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/payment" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Create Payment Method +func CreatePaymentMethodHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreatePaymentMethodRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := payment.NewCreatePaymentMethodLogic(c.Request.Context(), svcCtx) + resp, err := l.CreatePaymentMethod(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/payment/deletePaymentMethodHandler.go b/internal/handler/admin/payment/deletePaymentMethodHandler.go new file mode 100644 index 0000000..b3d4365 --- /dev/null +++ b/internal/handler/admin/payment/deletePaymentMethodHandler.go @@ -0,0 +1,26 @@ +package payment + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/payment" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Delete Payment Method +func DeletePaymentMethodHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeletePaymentMethodRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := payment.NewDeletePaymentMethodLogic(c.Request.Context(), svcCtx) + err := l.DeletePaymentMethod(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/payment/getPaymentMethodListHandler.go b/internal/handler/admin/payment/getPaymentMethodListHandler.go new file mode 100644 index 0000000..c968f53 --- /dev/null +++ b/internal/handler/admin/payment/getPaymentMethodListHandler.go @@ -0,0 +1,26 @@ +package payment + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/payment" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// GetPaymentMethodListHandler Get Payment Method List +func GetPaymentMethodListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetPaymentMethodListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := payment.NewGetPaymentMethodListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetPaymentMethodList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/payment/getPaymentPlatformHandler.go b/internal/handler/admin/payment/getPaymentPlatformHandler.go new file mode 100644 index 0000000..7e8d92e --- /dev/null +++ b/internal/handler/admin/payment/getPaymentPlatformHandler.go @@ -0,0 +1,18 @@ +package payment + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/payment" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get supported payment platform +func GetPaymentPlatformHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := payment.NewGetPaymentPlatformLogic(c.Request.Context(), svcCtx) + resp, err := l.GetPaymentPlatform() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/payment/updatePaymentMethodHandler.go b/internal/handler/admin/payment/updatePaymentMethodHandler.go new file mode 100644 index 0000000..10b7802 --- /dev/null +++ b/internal/handler/admin/payment/updatePaymentMethodHandler.go @@ -0,0 +1,26 @@ +package payment + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/payment" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update Payment Method +func UpdatePaymentMethodHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdatePaymentMethodRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := payment.NewUpdatePaymentMethodLogic(c.Request.Context(), svcCtx) + resp, err := l.UpdatePaymentMethod(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/server/batchDeleteNodeGroupHandler.go b/internal/handler/admin/server/batchDeleteNodeGroupHandler.go new file mode 100644 index 0000000..caa6411 --- /dev/null +++ b/internal/handler/admin/server/batchDeleteNodeGroupHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Batch delete node group +func BatchDeleteNodeGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.BatchDeleteNodeGroupRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewBatchDeleteNodeGroupLogic(c.Request.Context(), svcCtx) + err := l.BatchDeleteNodeGroup(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/server/batchDeleteNodeHandler.go b/internal/handler/admin/server/batchDeleteNodeHandler.go new file mode 100644 index 0000000..221fe86 --- /dev/null +++ b/internal/handler/admin/server/batchDeleteNodeHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Batch delete node +func BatchDeleteNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.BatchDeleteNodeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewBatchDeleteNodeLogic(c.Request.Context(), svcCtx) + err := l.BatchDeleteNode(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/server/createNodeGroupHandler.go b/internal/handler/admin/server/createNodeGroupHandler.go new file mode 100644 index 0000000..1523833 --- /dev/null +++ b/internal/handler/admin/server/createNodeGroupHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Create node group +func CreateNodeGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateNodeGroupRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewCreateNodeGroupLogic(c.Request.Context(), svcCtx) + err := l.CreateNodeGroup(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/server/createNodeHandler.go b/internal/handler/admin/server/createNodeHandler.go new file mode 100644 index 0000000..d4b8a98 --- /dev/null +++ b/internal/handler/admin/server/createNodeHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Create node +func CreateNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateNodeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewCreateNodeLogic(c.Request.Context(), svcCtx) + err := l.CreateNode(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/server/createRuleGroupHandler.go b/internal/handler/admin/server/createRuleGroupHandler.go new file mode 100644 index 0000000..21c02c4 --- /dev/null +++ b/internal/handler/admin/server/createRuleGroupHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Create rule group +func CreateRuleGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateRuleGroupRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewCreateRuleGroupLogic(c.Request.Context(), svcCtx) + err := l.CreateRuleGroup(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/server/deleteNodeGroupHandler.go b/internal/handler/admin/server/deleteNodeGroupHandler.go new file mode 100644 index 0000000..492898f --- /dev/null +++ b/internal/handler/admin/server/deleteNodeGroupHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Delete node group +func DeleteNodeGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeleteNodeGroupRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewDeleteNodeGroupLogic(c.Request.Context(), svcCtx) + err := l.DeleteNodeGroup(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/server/deleteNodeHandler.go b/internal/handler/admin/server/deleteNodeHandler.go new file mode 100644 index 0000000..62f4a60 --- /dev/null +++ b/internal/handler/admin/server/deleteNodeHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Delete node +func DeleteNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeleteNodeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewDeleteNodeLogic(c.Request.Context(), svcCtx) + err := l.DeleteNode(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/server/deleteRuleGroupHandler.go b/internal/handler/admin/server/deleteRuleGroupHandler.go new file mode 100644 index 0000000..91b830e --- /dev/null +++ b/internal/handler/admin/server/deleteRuleGroupHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Delete rule group +func DeleteRuleGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeleteRuleGroupRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewDeleteRuleGroupLogic(c.Request.Context(), svcCtx) + err := l.DeleteRuleGroup(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/server/getNodeDetailHandler.go b/internal/handler/admin/server/getNodeDetailHandler.go new file mode 100644 index 0000000..8485a73 --- /dev/null +++ b/internal/handler/admin/server/getNodeDetailHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get node detail +func GetNodeDetailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetDetailRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewGetNodeDetailLogic(c.Request.Context(), svcCtx) + resp, err := l.GetNodeDetail(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/server/getNodeGroupListHandler.go b/internal/handler/admin/server/getNodeGroupListHandler.go new file mode 100644 index 0000000..99bdf3a --- /dev/null +++ b/internal/handler/admin/server/getNodeGroupListHandler.go @@ -0,0 +1,18 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get node group list +func GetNodeGroupListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := server.NewGetNodeGroupListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetNodeGroupList() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/server/getNodeListHandler.go b/internal/handler/admin/server/getNodeListHandler.go new file mode 100644 index 0000000..2c47f74 --- /dev/null +++ b/internal/handler/admin/server/getNodeListHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get node list +func GetNodeListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetNodeServerListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewGetNodeListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetNodeList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/server/getNodeTagListHandler.go b/internal/handler/admin/server/getNodeTagListHandler.go new file mode 100644 index 0000000..700d79b --- /dev/null +++ b/internal/handler/admin/server/getNodeTagListHandler.go @@ -0,0 +1,18 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get node tag list +func GetNodeTagListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := server.NewGetNodeTagListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetNodeTagList() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/server/getRuleGroupListHandler.go b/internal/handler/admin/server/getRuleGroupListHandler.go new file mode 100644 index 0000000..22b14cf --- /dev/null +++ b/internal/handler/admin/server/getRuleGroupListHandler.go @@ -0,0 +1,18 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get rule group list +func GetRuleGroupListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := server.NewGetRuleGroupListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetRuleGroupList() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/server/nodeSortHandler.go b/internal/handler/admin/server/nodeSortHandler.go new file mode 100644 index 0000000..84c5184 --- /dev/null +++ b/internal/handler/admin/server/nodeSortHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Node sort +func NodeSortHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.NodeSortRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewNodeSortLogic(c.Request.Context(), svcCtx) + err := l.NodeSort(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/server/updateNodeGroupHandler.go b/internal/handler/admin/server/updateNodeGroupHandler.go new file mode 100644 index 0000000..4b4d2de --- /dev/null +++ b/internal/handler/admin/server/updateNodeGroupHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update node group +func UpdateNodeGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateNodeGroupRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewUpdateNodeGroupLogic(c.Request.Context(), svcCtx) + err := l.UpdateNodeGroup(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/server/updateNodeHandler.go b/internal/handler/admin/server/updateNodeHandler.go new file mode 100644 index 0000000..8ced9af --- /dev/null +++ b/internal/handler/admin/server/updateNodeHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update node +func UpdateNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateNodeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewUpdateNodeLogic(c.Request.Context(), svcCtx) + err := l.UpdateNode(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/server/updateRuleGroupHandler.go b/internal/handler/admin/server/updateRuleGroupHandler.go new file mode 100644 index 0000000..e77cfc1 --- /dev/null +++ b/internal/handler/admin/server/updateRuleGroupHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update rule group +func UpdateRuleGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateRuleGroupRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewUpdateRuleGroupLogic(c.Request.Context(), svcCtx) + err := l.UpdateRuleGroup(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/subscribe/batchDeleteSubscribeGroupHandler.go b/internal/handler/admin/subscribe/batchDeleteSubscribeGroupHandler.go new file mode 100644 index 0000000..1d2a3c9 --- /dev/null +++ b/internal/handler/admin/subscribe/batchDeleteSubscribeGroupHandler.go @@ -0,0 +1,26 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Batch delete subscribe group +func BatchDeleteSubscribeGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.BatchDeleteSubscribeGroupRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := subscribe.NewBatchDeleteSubscribeGroupLogic(c.Request.Context(), svcCtx) + err := l.BatchDeleteSubscribeGroup(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/subscribe/batchDeleteSubscribeHandler.go b/internal/handler/admin/subscribe/batchDeleteSubscribeHandler.go new file mode 100644 index 0000000..e16bb2c --- /dev/null +++ b/internal/handler/admin/subscribe/batchDeleteSubscribeHandler.go @@ -0,0 +1,26 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Batch delete subscribe +func BatchDeleteSubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.BatchDeleteSubscribeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := subscribe.NewBatchDeleteSubscribeLogic(c.Request.Context(), svcCtx) + err := l.BatchDeleteSubscribe(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/subscribe/createSubscribeGroupHandler.go b/internal/handler/admin/subscribe/createSubscribeGroupHandler.go new file mode 100644 index 0000000..c0dd388 --- /dev/null +++ b/internal/handler/admin/subscribe/createSubscribeGroupHandler.go @@ -0,0 +1,26 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Create subscribe group +func CreateSubscribeGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateSubscribeGroupRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := subscribe.NewCreateSubscribeGroupLogic(c.Request.Context(), svcCtx) + err := l.CreateSubscribeGroup(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/subscribe/createSubscribeHandler.go b/internal/handler/admin/subscribe/createSubscribeHandler.go new file mode 100644 index 0000000..95c7c83 --- /dev/null +++ b/internal/handler/admin/subscribe/createSubscribeHandler.go @@ -0,0 +1,26 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Create subscribe +func CreateSubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateSubscribeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := subscribe.NewCreateSubscribeLogic(c.Request.Context(), svcCtx) + err := l.CreateSubscribe(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/subscribe/deleteSubscribeGroupHandler.go b/internal/handler/admin/subscribe/deleteSubscribeGroupHandler.go new file mode 100644 index 0000000..54d20df --- /dev/null +++ b/internal/handler/admin/subscribe/deleteSubscribeGroupHandler.go @@ -0,0 +1,26 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Delete subscribe group +func DeleteSubscribeGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeleteSubscribeGroupRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := subscribe.NewDeleteSubscribeGroupLogic(c.Request.Context(), svcCtx) + err := l.DeleteSubscribeGroup(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/subscribe/deleteSubscribeHandler.go b/internal/handler/admin/subscribe/deleteSubscribeHandler.go new file mode 100644 index 0000000..b25b1cb --- /dev/null +++ b/internal/handler/admin/subscribe/deleteSubscribeHandler.go @@ -0,0 +1,26 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Delete subscribe +func DeleteSubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeleteSubscribeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := subscribe.NewDeleteSubscribeLogic(c.Request.Context(), svcCtx) + err := l.DeleteSubscribe(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/subscribe/getSubscribeDetailsHandler.go b/internal/handler/admin/subscribe/getSubscribeDetailsHandler.go new file mode 100644 index 0000000..216e753 --- /dev/null +++ b/internal/handler/admin/subscribe/getSubscribeDetailsHandler.go @@ -0,0 +1,26 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get subscribe details +func GetSubscribeDetailsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetSubscribeDetailsRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := subscribe.NewGetSubscribeDetailsLogic(c.Request.Context(), svcCtx) + resp, err := l.GetSubscribeDetails(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/subscribe/getSubscribeGroupListHandler.go b/internal/handler/admin/subscribe/getSubscribeGroupListHandler.go new file mode 100644 index 0000000..980dbb4 --- /dev/null +++ b/internal/handler/admin/subscribe/getSubscribeGroupListHandler.go @@ -0,0 +1,18 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get subscribe group list +func GetSubscribeGroupListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := subscribe.NewGetSubscribeGroupListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetSubscribeGroupList() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/subscribe/getSubscribeListHandler.go b/internal/handler/admin/subscribe/getSubscribeListHandler.go new file mode 100644 index 0000000..eedf89d --- /dev/null +++ b/internal/handler/admin/subscribe/getSubscribeListHandler.go @@ -0,0 +1,26 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get subscribe list +func GetSubscribeListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetSubscribeListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := subscribe.NewGetSubscribeListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetSubscribeList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/subscribe/subscribeSortHandler.go b/internal/handler/admin/subscribe/subscribeSortHandler.go new file mode 100644 index 0000000..8f7c5b6 --- /dev/null +++ b/internal/handler/admin/subscribe/subscribeSortHandler.go @@ -0,0 +1,26 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Subscribe sort +func SubscribeSortHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.SubscribeSortRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := subscribe.NewSubscribeSortLogic(c.Request.Context(), svcCtx) + err := l.SubscribeSort(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/subscribe/updateSubscribeGroupHandler.go b/internal/handler/admin/subscribe/updateSubscribeGroupHandler.go new file mode 100644 index 0000000..53d9fd1 --- /dev/null +++ b/internal/handler/admin/subscribe/updateSubscribeGroupHandler.go @@ -0,0 +1,26 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update subscribe group +func UpdateSubscribeGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateSubscribeGroupRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := subscribe.NewUpdateSubscribeGroupLogic(c.Request.Context(), svcCtx) + err := l.UpdateSubscribeGroup(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/subscribe/updateSubscribeHandler.go b/internal/handler/admin/subscribe/updateSubscribeHandler.go new file mode 100644 index 0000000..c01bcc9 --- /dev/null +++ b/internal/handler/admin/subscribe/updateSubscribeHandler.go @@ -0,0 +1,26 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update subscribe +func UpdateSubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateSubscribeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := subscribe.NewUpdateSubscribeLogic(c.Request.Context(), svcCtx) + err := l.UpdateSubscribe(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/system/createApplicationHandler.go b/internal/handler/admin/system/createApplicationHandler.go new file mode 100644 index 0000000..f2745f1 --- /dev/null +++ b/internal/handler/admin/system/createApplicationHandler.go @@ -0,0 +1,26 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Create application +func CreateApplicationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateApplicationRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := system.NewCreateApplicationLogic(c.Request.Context(), svcCtx) + err := l.CreateApplication(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/system/createApplicationVersionHandler.go b/internal/handler/admin/system/createApplicationVersionHandler.go new file mode 100644 index 0000000..bbae577 --- /dev/null +++ b/internal/handler/admin/system/createApplicationVersionHandler.go @@ -0,0 +1,26 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Create application version +func CreateApplicationVersionHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateApplicationVersionRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := system.NewCreateApplicationVersionLogic(c.Request.Context(), svcCtx) + err := l.CreateApplicationVersion(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/system/deleteApplicationHandler.go b/internal/handler/admin/system/deleteApplicationHandler.go new file mode 100644 index 0000000..3de1aa4 --- /dev/null +++ b/internal/handler/admin/system/deleteApplicationHandler.go @@ -0,0 +1,26 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Delete application +func DeleteApplicationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeleteApplicationRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := system.NewDeleteApplicationLogic(c.Request.Context(), svcCtx) + err := l.DeleteApplication(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/system/deleteApplicationVersionHandler.go b/internal/handler/admin/system/deleteApplicationVersionHandler.go new file mode 100644 index 0000000..7f9516f --- /dev/null +++ b/internal/handler/admin/system/deleteApplicationVersionHandler.go @@ -0,0 +1,26 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Delete application +func DeleteApplicationVersionHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeleteApplicationVersionRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := system.NewDeleteApplicationVersionLogic(c.Request.Context(), svcCtx) + err := l.DeleteApplicationVersion(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/system/getApplicationConfigHandler.go b/internal/handler/admin/system/getApplicationConfigHandler.go new file mode 100644 index 0000000..0f8ddab --- /dev/null +++ b/internal/handler/admin/system/getApplicationConfigHandler.go @@ -0,0 +1,18 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// get application config +func GetApplicationConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := system.NewGetApplicationConfigLogic(c.Request.Context(), svcCtx) + resp, err := l.GetApplicationConfig() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/system/getApplicationHandler.go b/internal/handler/admin/system/getApplicationHandler.go new file mode 100644 index 0000000..a0eefbd --- /dev/null +++ b/internal/handler/admin/system/getApplicationHandler.go @@ -0,0 +1,18 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get application +func GetApplicationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := system.NewGetApplicationLogic(c.Request.Context(), svcCtx) + resp, err := l.GetApplication() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/system/getCurrencyConfigHandler.go b/internal/handler/admin/system/getCurrencyConfigHandler.go new file mode 100644 index 0000000..a172227 --- /dev/null +++ b/internal/handler/admin/system/getCurrencyConfigHandler.go @@ -0,0 +1,18 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get Currency Config +func GetCurrencyConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := system.NewGetCurrencyConfigLogic(c.Request.Context(), svcCtx) + resp, err := l.GetCurrencyConfig() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/system/getInviteConfigHandler.go b/internal/handler/admin/system/getInviteConfigHandler.go new file mode 100644 index 0000000..f586852 --- /dev/null +++ b/internal/handler/admin/system/getInviteConfigHandler.go @@ -0,0 +1,18 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get invite config +func GetInviteConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := system.NewGetInviteConfigLogic(c.Request.Context(), svcCtx) + resp, err := l.GetInviteConfig() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/system/getNodeConfigHandler.go b/internal/handler/admin/system/getNodeConfigHandler.go new file mode 100644 index 0000000..0a0aba6 --- /dev/null +++ b/internal/handler/admin/system/getNodeConfigHandler.go @@ -0,0 +1,18 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get node config +func GetNodeConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := system.NewGetNodeConfigLogic(c.Request.Context(), svcCtx) + resp, err := l.GetNodeConfig() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/system/getNodeMultiplierHandler.go b/internal/handler/admin/system/getNodeMultiplierHandler.go new file mode 100644 index 0000000..5268a6a --- /dev/null +++ b/internal/handler/admin/system/getNodeMultiplierHandler.go @@ -0,0 +1,18 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get Node Multiplier +func GetNodeMultiplierHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := system.NewGetNodeMultiplierLogic(c.Request.Context(), svcCtx) + resp, err := l.GetNodeMultiplier() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/system/getPrivacyPolicyConfigHandler.go b/internal/handler/admin/system/getPrivacyPolicyConfigHandler.go new file mode 100644 index 0000000..3f86383 --- /dev/null +++ b/internal/handler/admin/system/getPrivacyPolicyConfigHandler.go @@ -0,0 +1,18 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// get Privacy Policy Config +func GetPrivacyPolicyConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := system.NewGetPrivacyPolicyConfigLogic(c.Request.Context(), svcCtx) + resp, err := l.GetPrivacyPolicyConfig() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/system/getRegisterConfigHandler.go b/internal/handler/admin/system/getRegisterConfigHandler.go new file mode 100644 index 0000000..b8b9687 --- /dev/null +++ b/internal/handler/admin/system/getRegisterConfigHandler.go @@ -0,0 +1,18 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get register config +func GetRegisterConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := system.NewGetRegisterConfigLogic(c.Request.Context(), svcCtx) + resp, err := l.GetRegisterConfig() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/system/getSiteConfigHandler.go b/internal/handler/admin/system/getSiteConfigHandler.go new file mode 100644 index 0000000..109a1ee --- /dev/null +++ b/internal/handler/admin/system/getSiteConfigHandler.go @@ -0,0 +1,18 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get site config +func GetSiteConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := system.NewGetSiteConfigLogic(c.Request.Context(), svcCtx) + resp, err := l.GetSiteConfig() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/system/getSubscribeConfigHandler.go b/internal/handler/admin/system/getSubscribeConfigHandler.go new file mode 100644 index 0000000..0b5fb19 --- /dev/null +++ b/internal/handler/admin/system/getSubscribeConfigHandler.go @@ -0,0 +1,18 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get subscribe config +func GetSubscribeConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := system.NewGetSubscribeConfigLogic(c.Request.Context(), svcCtx) + resp, err := l.GetSubscribeConfig() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/system/getSubscribeTypeHandler.go b/internal/handler/admin/system/getSubscribeTypeHandler.go new file mode 100644 index 0000000..f017cf7 --- /dev/null +++ b/internal/handler/admin/system/getSubscribeTypeHandler.go @@ -0,0 +1,18 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get subscribe type +func GetSubscribeTypeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := system.NewGetSubscribeTypeLogic(c.Request.Context(), svcCtx) + resp, err := l.GetSubscribeType() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/system/getTosConfigHandler.go b/internal/handler/admin/system/getTosConfigHandler.go new file mode 100644 index 0000000..06c6cc0 --- /dev/null +++ b/internal/handler/admin/system/getTosConfigHandler.go @@ -0,0 +1,18 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get Team of Service Config +func GetTosConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := system.NewGetTosConfigLogic(c.Request.Context(), svcCtx) + resp, err := l.GetTosConfig() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/system/getVerifyCodeConfigHandler.go b/internal/handler/admin/system/getVerifyCodeConfigHandler.go new file mode 100644 index 0000000..4a23c1b --- /dev/null +++ b/internal/handler/admin/system/getVerifyCodeConfigHandler.go @@ -0,0 +1,18 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get Verify Code Config +func GetVerifyCodeConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := system.NewGetVerifyCodeConfigLogic(c.Request.Context(), svcCtx) + resp, err := l.GetVerifyCodeConfig() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/system/getVerifyConfigHandler.go b/internal/handler/admin/system/getVerifyConfigHandler.go new file mode 100644 index 0000000..eb928dd --- /dev/null +++ b/internal/handler/admin/system/getVerifyConfigHandler.go @@ -0,0 +1,18 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get verify config +func GetVerifyConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := system.NewGetVerifyConfigLogic(c.Request.Context(), svcCtx) + resp, err := l.GetVerifyConfig() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/system/setNodeMultiplierHandler.go b/internal/handler/admin/system/setNodeMultiplierHandler.go new file mode 100644 index 0000000..2b01f46 --- /dev/null +++ b/internal/handler/admin/system/setNodeMultiplierHandler.go @@ -0,0 +1,26 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Set Node Multiplier +func SetNodeMultiplierHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.SetNodeMultiplierRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := system.NewSetNodeMultiplierLogic(c.Request.Context(), svcCtx) + err := l.SetNodeMultiplier(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/system/settingTelegramBotHandler.go b/internal/handler/admin/system/settingTelegramBotHandler.go new file mode 100644 index 0000000..aac5cc4 --- /dev/null +++ b/internal/handler/admin/system/settingTelegramBotHandler.go @@ -0,0 +1,18 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// setting telegram bot +func SettingTelegramBotHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := system.NewSettingTelegramBotLogic(c.Request.Context(), svcCtx) + err := l.SettingTelegramBot() + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/system/updateApplicationConfigHandler.go b/internal/handler/admin/system/updateApplicationConfigHandler.go new file mode 100644 index 0000000..84a296e --- /dev/null +++ b/internal/handler/admin/system/updateApplicationConfigHandler.go @@ -0,0 +1,26 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// update application config +func UpdateApplicationConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.ApplicationConfig + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := system.NewUpdateApplicationConfigLogic(c.Request.Context(), svcCtx) + err := l.UpdateApplicationConfig(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/system/updateApplicationHandler.go b/internal/handler/admin/system/updateApplicationHandler.go new file mode 100644 index 0000000..f7a0de0 --- /dev/null +++ b/internal/handler/admin/system/updateApplicationHandler.go @@ -0,0 +1,26 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update application +func UpdateApplicationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateApplicationRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := system.NewUpdateApplicationLogic(c.Request.Context(), svcCtx) + err := l.UpdateApplication(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/system/updateApplicationVersionHandler.go b/internal/handler/admin/system/updateApplicationVersionHandler.go new file mode 100644 index 0000000..fffe6e3 --- /dev/null +++ b/internal/handler/admin/system/updateApplicationVersionHandler.go @@ -0,0 +1,26 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update application version +func UpdateApplicationVersionHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateApplicationVersionRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := system.NewUpdateApplicationVersionLogic(c.Request.Context(), svcCtx) + err := l.UpdateApplicationVersion(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/system/updateCurrencyConfigHandler.go b/internal/handler/admin/system/updateCurrencyConfigHandler.go new file mode 100644 index 0000000..1bcae53 --- /dev/null +++ b/internal/handler/admin/system/updateCurrencyConfigHandler.go @@ -0,0 +1,26 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update Currency Config +func UpdateCurrencyConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CurrencyConfig + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := system.NewUpdateCurrencyConfigLogic(c.Request.Context(), svcCtx) + err := l.UpdateCurrencyConfig(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/system/updateInviteConfigHandler.go b/internal/handler/admin/system/updateInviteConfigHandler.go new file mode 100644 index 0000000..b9e6bbb --- /dev/null +++ b/internal/handler/admin/system/updateInviteConfigHandler.go @@ -0,0 +1,26 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update invite config +func UpdateInviteConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.InviteConfig + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := system.NewUpdateInviteConfigLogic(c.Request.Context(), svcCtx) + err := l.UpdateInviteConfig(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/system/updateNodeConfigHandler.go b/internal/handler/admin/system/updateNodeConfigHandler.go new file mode 100644 index 0000000..ceb95de --- /dev/null +++ b/internal/handler/admin/system/updateNodeConfigHandler.go @@ -0,0 +1,26 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update node config +func UpdateNodeConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.NodeConfig + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := system.NewUpdateNodeConfigLogic(c.Request.Context(), svcCtx) + err := l.UpdateNodeConfig(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/system/updatePrivacyPolicyConfigHandler.go b/internal/handler/admin/system/updatePrivacyPolicyConfigHandler.go new file mode 100644 index 0000000..ca5168b --- /dev/null +++ b/internal/handler/admin/system/updatePrivacyPolicyConfigHandler.go @@ -0,0 +1,26 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update Privacy Policy Config +func UpdatePrivacyPolicyConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.PrivacyPolicyConfig + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := system.NewUpdatePrivacyPolicyConfigLogic(c.Request.Context(), svcCtx) + err := l.UpdatePrivacyPolicyConfig(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/system/updateRegisterConfigHandler.go b/internal/handler/admin/system/updateRegisterConfigHandler.go new file mode 100644 index 0000000..507b7b9 --- /dev/null +++ b/internal/handler/admin/system/updateRegisterConfigHandler.go @@ -0,0 +1,26 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update register config +func UpdateRegisterConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.RegisterConfig + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := system.NewUpdateRegisterConfigLogic(c.Request.Context(), svcCtx) + err := l.UpdateRegisterConfig(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/system/updateSiteConfigHandler.go b/internal/handler/admin/system/updateSiteConfigHandler.go new file mode 100644 index 0000000..8ecb211 --- /dev/null +++ b/internal/handler/admin/system/updateSiteConfigHandler.go @@ -0,0 +1,26 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update site config +func UpdateSiteConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.SiteConfig + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := system.NewUpdateSiteConfigLogic(c.Request.Context(), svcCtx) + err := l.UpdateSiteConfig(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/system/updateSubscribeConfigHandler.go b/internal/handler/admin/system/updateSubscribeConfigHandler.go new file mode 100644 index 0000000..185302b --- /dev/null +++ b/internal/handler/admin/system/updateSubscribeConfigHandler.go @@ -0,0 +1,26 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update subscribe config +func UpdateSubscribeConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.SubscribeConfig + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := system.NewUpdateSubscribeConfigLogic(c.Request.Context(), svcCtx) + err := l.UpdateSubscribeConfig(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/system/updateTosConfigHandler.go b/internal/handler/admin/system/updateTosConfigHandler.go new file mode 100644 index 0000000..16c71e2 --- /dev/null +++ b/internal/handler/admin/system/updateTosConfigHandler.go @@ -0,0 +1,26 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update Team of Service Config +func UpdateTosConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.TosConfig + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := system.NewUpdateTosConfigLogic(c.Request.Context(), svcCtx) + err := l.UpdateTosConfig(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/system/updateVerifyCodeConfigHandler.go b/internal/handler/admin/system/updateVerifyCodeConfigHandler.go new file mode 100644 index 0000000..3c27154 --- /dev/null +++ b/internal/handler/admin/system/updateVerifyCodeConfigHandler.go @@ -0,0 +1,26 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update Verify Code Config +func UpdateVerifyCodeConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.VerifyCodeConfig + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := system.NewUpdateVerifyCodeConfigLogic(c.Request.Context(), svcCtx) + err := l.UpdateVerifyCodeConfig(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/system/updateVerifyConfigHandler.go b/internal/handler/admin/system/updateVerifyConfigHandler.go new file mode 100644 index 0000000..483985c --- /dev/null +++ b/internal/handler/admin/system/updateVerifyConfigHandler.go @@ -0,0 +1,26 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update verify config +func UpdateVerifyConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.VerifyConfig + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := system.NewUpdateVerifyConfigLogic(c.Request.Context(), svcCtx) + err := l.UpdateVerifyConfig(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/ticket/createTicketFollowHandler.go b/internal/handler/admin/ticket/createTicketFollowHandler.go new file mode 100644 index 0000000..34b6b2d --- /dev/null +++ b/internal/handler/admin/ticket/createTicketFollowHandler.go @@ -0,0 +1,26 @@ +package ticket + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/ticket" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Create ticket follow +func CreateTicketFollowHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateTicketFollowRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := ticket.NewCreateTicketFollowLogic(c.Request.Context(), svcCtx) + err := l.CreateTicketFollow(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/ticket/getTicketHandler.go b/internal/handler/admin/ticket/getTicketHandler.go new file mode 100644 index 0000000..5058a7f --- /dev/null +++ b/internal/handler/admin/ticket/getTicketHandler.go @@ -0,0 +1,26 @@ +package ticket + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/ticket" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get ticket detail +func GetTicketHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetTicketRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := ticket.NewGetTicketLogic(c.Request.Context(), svcCtx) + resp, err := l.GetTicket(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/ticket/getTicketListHandler.go b/internal/handler/admin/ticket/getTicketListHandler.go new file mode 100644 index 0000000..bfca05c --- /dev/null +++ b/internal/handler/admin/ticket/getTicketListHandler.go @@ -0,0 +1,26 @@ +package ticket + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/ticket" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get ticket list +func GetTicketListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetTicketListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := ticket.NewGetTicketListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetTicketList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/ticket/updateTicketStatusHandler.go b/internal/handler/admin/ticket/updateTicketStatusHandler.go new file mode 100644 index 0000000..f710b98 --- /dev/null +++ b/internal/handler/admin/ticket/updateTicketStatusHandler.go @@ -0,0 +1,26 @@ +package ticket + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/ticket" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update ticket status +func UpdateTicketStatusHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateTicketStatusRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := ticket.NewUpdateTicketStatusLogic(c.Request.Context(), svcCtx) + err := l.UpdateTicketStatus(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/tool/getSystemLogHandler.go b/internal/handler/admin/tool/getSystemLogHandler.go new file mode 100644 index 0000000..80dd1d3 --- /dev/null +++ b/internal/handler/admin/tool/getSystemLogHandler.go @@ -0,0 +1,18 @@ +package tool + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/tool" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get System Log +func GetSystemLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := tool.NewGetSystemLogLogic(c.Request.Context(), svcCtx) + resp, err := l.GetSystemLog() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/tool/restartSystemHandler.go b/internal/handler/admin/tool/restartSystemHandler.go new file mode 100644 index 0000000..5d391b0 --- /dev/null +++ b/internal/handler/admin/tool/restartSystemHandler.go @@ -0,0 +1,18 @@ +package tool + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/tool" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Restart System +func RestartSystemHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := tool.NewRestartSystemLogic(c.Request.Context(), svcCtx) + err := l.RestartSystem() + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/user/batchDeleteUserHandler.go b/internal/handler/admin/user/batchDeleteUserHandler.go new file mode 100644 index 0000000..d0f1c65 --- /dev/null +++ b/internal/handler/admin/user/batchDeleteUserHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Batch delete user +func BatchDeleteUserHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.BatchDeleteUserRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewBatchDeleteUserLogic(c.Request.Context(), svcCtx) + err := l.BatchDeleteUser(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/user/createUserAuthMethodHandler.go b/internal/handler/admin/user/createUserAuthMethodHandler.go new file mode 100644 index 0000000..de69a03 --- /dev/null +++ b/internal/handler/admin/user/createUserAuthMethodHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Create user auth method +func CreateUserAuthMethodHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateUserAuthMethodRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewCreateUserAuthMethodLogic(c.Request.Context(), svcCtx) + err := l.CreateUserAuthMethod(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/user/createUserHandler.go b/internal/handler/admin/user/createUserHandler.go new file mode 100644 index 0000000..4de5e76 --- /dev/null +++ b/internal/handler/admin/user/createUserHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Create user +func CreateUserHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateUserRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewCreateUserLogic(c.Request.Context(), svcCtx) + err := l.CreateUser(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/user/createUserSubscribeHandler.go b/internal/handler/admin/user/createUserSubscribeHandler.go new file mode 100644 index 0000000..556c75c --- /dev/null +++ b/internal/handler/admin/user/createUserSubscribeHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Create user subcribe +func CreateUserSubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateUserSubscribeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewCreateUserSubscribeLogic(c.Request.Context(), svcCtx) + err := l.CreateUserSubscribe(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/user/currentUserHandler.go b/internal/handler/admin/user/currentUserHandler.go new file mode 100644 index 0000000..3851558 --- /dev/null +++ b/internal/handler/admin/user/currentUserHandler.go @@ -0,0 +1,17 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Current user +func CurrentUserHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + l := user.NewCurrentUserLogic(c.Request.Context(), svcCtx) + resp, err := l.CurrentUser() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/user/deleteUserAuthMethodHandler.go b/internal/handler/admin/user/deleteUserAuthMethodHandler.go new file mode 100644 index 0000000..f9cde21 --- /dev/null +++ b/internal/handler/admin/user/deleteUserAuthMethodHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Delete user auth method +func DeleteUserAuthMethodHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeleteUserAuthMethodRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewDeleteUserAuthMethodLogic(c.Request.Context(), svcCtx) + err := l.DeleteUserAuthMethod(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/user/deleteUserDeviceHandler.go b/internal/handler/admin/user/deleteUserDeviceHandler.go new file mode 100644 index 0000000..6269cbe --- /dev/null +++ b/internal/handler/admin/user/deleteUserDeviceHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Delete user device +func DeleteUserDeviceHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeleteUserDeivceRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewDeleteUserDeviceLogic(c.Request.Context(), svcCtx) + err := l.DeleteUserDevice(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/user/deleteUserHandler.go b/internal/handler/admin/user/deleteUserHandler.go new file mode 100644 index 0000000..7e077b1 --- /dev/null +++ b/internal/handler/admin/user/deleteUserHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Delete user +func DeleteUserHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetDetailRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewDeleteUserLogic(c.Request.Context(), svcCtx) + err := l.DeleteUser(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/user/deleteUserSubscribeHandler.go b/internal/handler/admin/user/deleteUserSubscribeHandler.go new file mode 100644 index 0000000..9ff53da --- /dev/null +++ b/internal/handler/admin/user/deleteUserSubscribeHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Delete user subcribe +func DeleteUserSubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeleteUserSubscribeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewDeleteUserSubscribeLogic(c.Request.Context(), svcCtx) + err := l.DeleteUserSubscribe(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/user/getUserAuthMethodHandler.go b/internal/handler/admin/user/getUserAuthMethodHandler.go new file mode 100644 index 0000000..9e5c60c --- /dev/null +++ b/internal/handler/admin/user/getUserAuthMethodHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get user auth method +func GetUserAuthMethodHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetUserAuthMethodRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewGetUserAuthMethodLogic(c.Request.Context(), svcCtx) + resp, err := l.GetUserAuthMethod(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/user/getUserDetailHandler.go b/internal/handler/admin/user/getUserDetailHandler.go new file mode 100644 index 0000000..30a8f50 --- /dev/null +++ b/internal/handler/admin/user/getUserDetailHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get user detail +func GetUserDetailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetDetailRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewGetUserDetailLogic(c.Request.Context(), svcCtx) + resp, err := l.GetUserDetail(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/user/getUserListHandler.go b/internal/handler/admin/user/getUserListHandler.go new file mode 100644 index 0000000..6af432f --- /dev/null +++ b/internal/handler/admin/user/getUserListHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get user list +func GetUserListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetUserListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewGetUserListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetUserList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/user/getUserLoginLogsHandler.go b/internal/handler/admin/user/getUserLoginLogsHandler.go new file mode 100644 index 0000000..99646c7 --- /dev/null +++ b/internal/handler/admin/user/getUserLoginLogsHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get user login logs +func GetUserLoginLogsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetUserLoginLogsRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewGetUserLoginLogsLogic(c.Request.Context(), svcCtx) + resp, err := l.GetUserLoginLogs(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/user/getUserSubscribeByIdHandler.go b/internal/handler/admin/user/getUserSubscribeByIdHandler.go new file mode 100644 index 0000000..49df56a --- /dev/null +++ b/internal/handler/admin/user/getUserSubscribeByIdHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get user subcribe by id +func GetUserSubscribeByIdHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetUserSubscribeByIdRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewGetUserSubscribeByIdLogic(c.Request.Context(), svcCtx) + resp, err := l.GetUserSubscribeById(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/user/getUserSubscribeDevicesHandler.go b/internal/handler/admin/user/getUserSubscribeDevicesHandler.go new file mode 100644 index 0000000..cc342fb --- /dev/null +++ b/internal/handler/admin/user/getUserSubscribeDevicesHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get user subcribe devices +func GetUserSubscribeDevicesHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetUserSubscribeDevicesRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewGetUserSubscribeDevicesLogic(c.Request.Context(), svcCtx) + resp, err := l.GetUserSubscribeDevices(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/user/getUserSubscribeHandler.go b/internal/handler/admin/user/getUserSubscribeHandler.go new file mode 100644 index 0000000..1bf0125 --- /dev/null +++ b/internal/handler/admin/user/getUserSubscribeHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get user subcribe +func GetUserSubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetUserSubscribeListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewGetUserSubscribeLogic(c.Request.Context(), svcCtx) + resp, err := l.GetUserSubscribe(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/user/getUserSubscribeLogsHandler.go b/internal/handler/admin/user/getUserSubscribeLogsHandler.go new file mode 100644 index 0000000..a9a4263 --- /dev/null +++ b/internal/handler/admin/user/getUserSubscribeLogsHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get user subcribe logs +func GetUserSubscribeLogsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetUserSubscribeLogsRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewGetUserSubscribeLogsLogic(c.Request.Context(), svcCtx) + resp, err := l.GetUserSubscribeLogs(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/user/getUserSubscribeTrafficLogsHandler.go b/internal/handler/admin/user/getUserSubscribeTrafficLogsHandler.go new file mode 100644 index 0000000..f6cf5fb --- /dev/null +++ b/internal/handler/admin/user/getUserSubscribeTrafficLogsHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get user subcribe traffic logs +func GetUserSubscribeTrafficLogsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetUserSubscribeTrafficLogsRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewGetUserSubscribeTrafficLogsLogic(c.Request.Context(), svcCtx) + resp, err := l.GetUserSubscribeTrafficLogs(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/user/kickOfflineByUserDeviceHandler.go b/internal/handler/admin/user/kickOfflineByUserDeviceHandler.go new file mode 100644 index 0000000..ab9b4f6 --- /dev/null +++ b/internal/handler/admin/user/kickOfflineByUserDeviceHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// kick offline user device +func KickOfflineByUserDeviceHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.KickOfflineRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewKickOfflineByUserDeviceLogic(c.Request.Context(), svcCtx) + err := l.KickOfflineByUserDevice(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/user/updateUserAuthMethodHandler.go b/internal/handler/admin/user/updateUserAuthMethodHandler.go new file mode 100644 index 0000000..1f289c8 --- /dev/null +++ b/internal/handler/admin/user/updateUserAuthMethodHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update user auth method +func UpdateUserAuthMethodHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateUserAuthMethodRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewUpdateUserAuthMethodLogic(c.Request.Context(), svcCtx) + err := l.UpdateUserAuthMethod(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/user/updateUserBasicInfoHandler.go b/internal/handler/admin/user/updateUserBasicInfoHandler.go new file mode 100644 index 0000000..4187bfc --- /dev/null +++ b/internal/handler/admin/user/updateUserBasicInfoHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update user basic info +func UpdateUserBasicInfoHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateUserBasiceInfoRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewUpdateUserBasicInfoLogic(c.Request.Context(), svcCtx) + err := l.UpdateUserBasicInfo(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/user/updateUserDeviceHandler.go b/internal/handler/admin/user/updateUserDeviceHandler.go new file mode 100644 index 0000000..ae1b813 --- /dev/null +++ b/internal/handler/admin/user/updateUserDeviceHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// User device +func UpdateUserDeviceHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UserDevice + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewUpdateUserDeviceLogic(c.Request.Context(), svcCtx) + err := l.UpdateUserDevice(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/user/updateUserNotifySettingHandler.go b/internal/handler/admin/user/updateUserNotifySettingHandler.go new file mode 100644 index 0000000..3644c5b --- /dev/null +++ b/internal/handler/admin/user/updateUserNotifySettingHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update user notify setting +func UpdateUserNotifySettingHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateUserNotifySettingRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewUpdateUserNotifySettingLogic(c.Request.Context(), svcCtx) + err := l.UpdateUserNotifySetting(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/user/updateUserSubscribeHandler.go b/internal/handler/admin/user/updateUserSubscribeHandler.go new file mode 100644 index 0000000..c1bca31 --- /dev/null +++ b/internal/handler/admin/user/updateUserSubscribeHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update user subcribe +func UpdateUserSubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateUserSubscribeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewUpdateUserSubscribeLogic(c.Request.Context(), svcCtx) + err := l.UpdateUserSubscribe(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/app/announcement/queryannouncementhandler.go b/internal/handler/app/announcement/queryannouncementhandler.go new file mode 100644 index 0000000..7eec557 --- /dev/null +++ b/internal/handler/app/announcement/queryannouncementhandler.go @@ -0,0 +1,26 @@ +package announcement + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/announcement" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Query announcement +func QueryAnnouncementHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryAnnouncementRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := announcement.NewQueryAnnouncementLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryAnnouncement(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/auth/checkHandler.go b/internal/handler/app/auth/checkHandler.go new file mode 100644 index 0000000..6265f38 --- /dev/null +++ b/internal/handler/app/auth/checkHandler.go @@ -0,0 +1,26 @@ +package auth + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/auth" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Check Account +func CheckHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.AppAuthCheckRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := auth.NewCheckLogic(c, svcCtx) + resp, err := l.Check(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/auth/getAppConfigHandler.go b/internal/handler/app/auth/getAppConfigHandler.go new file mode 100644 index 0000000..83ea8c5 --- /dev/null +++ b/internal/handler/app/auth/getAppConfigHandler.go @@ -0,0 +1,26 @@ +package auth + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/auth" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// GetAppConfig +func GetAppConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.AppConfigRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := auth.NewGetAppConfigLogic(c, svcCtx) + resp, err := l.GetAppConfig(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/auth/loginHandler.go b/internal/handler/app/auth/loginHandler.go new file mode 100644 index 0000000..b6cbe1a --- /dev/null +++ b/internal/handler/app/auth/loginHandler.go @@ -0,0 +1,42 @@ +package auth + +import ( + "time" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/auth" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/ppanel-server/pkg/turnstile" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +// Login +func LoginHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.AppAuthRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + if svcCtx.Config.Verify.LoginVerify { + verifyTurns := turnstile.New(turnstile.Config{ + Secret: svcCtx.Config.Verify.TurnstileSecret, + Timeout: 3 * time.Second, + }) + if verify, err := verifyTurns.Verify(c, req.CfToken, c.ClientIP()); err != nil || !verify { + err = errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "error: %v, verify: %v", err, verify) + result.HttpResult(c, nil, err) + return + } + } + l := auth.NewLoginLogic(c, svcCtx) + resp, err := l.Login(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/auth/registerHandler.go b/internal/handler/app/auth/registerHandler.go new file mode 100644 index 0000000..530fb89 --- /dev/null +++ b/internal/handler/app/auth/registerHandler.go @@ -0,0 +1,43 @@ +package auth + +import ( + "time" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/auth" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/ppanel-server/pkg/turnstile" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +// Register +func RegisterHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.AppAuthRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + // get client ip + if svcCtx.Config.Verify.RegisterVerify { + verifyTurns := turnstile.New(turnstile.Config{ + Secret: svcCtx.Config.Verify.TurnstileSecret, + Timeout: 3 * time.Second, + }) + if verify, err := verifyTurns.Verify(c, req.CfToken, c.ClientIP()); err != nil || !verify { + err = errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "error: %v, verify: %v", err, verify) + result.HttpResult(c, nil, err) + return + } + } + + l := auth.NewRegisterLogic(c, svcCtx) + resp, err := l.Register(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/auth/resetPasswordHandler.go b/internal/handler/app/auth/resetPasswordHandler.go new file mode 100644 index 0000000..c1c8dd2 --- /dev/null +++ b/internal/handler/app/auth/resetPasswordHandler.go @@ -0,0 +1,41 @@ +package auth + +import ( + "time" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/auth" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/ppanel-server/pkg/turnstile" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +// Reset Password +func ResetPasswordHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.AppAuthRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + if svcCtx.Config.Verify.ResetPasswordVerify { + verifyTurns := turnstile.New(turnstile.Config{ + Secret: svcCtx.Config.Verify.TurnstileSecret, + Timeout: 3 * time.Second, + }) + if verify, err := verifyTurns.Verify(c, req.CfToken, c.ClientIP()); err != nil || !verify { + err = errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "error: %v, verify: %v", err, verify) + result.HttpResult(c, nil, err) + return + } + } + l := auth.NewResetPasswordLogic(c, svcCtx) + resp, err := l.ResetPassword(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/document/querydocumentdetailhandler.go b/internal/handler/app/document/querydocumentdetailhandler.go new file mode 100644 index 0000000..36050e4 --- /dev/null +++ b/internal/handler/app/document/querydocumentdetailhandler.go @@ -0,0 +1,26 @@ +package document + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/document" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get document detail +func QueryDocumentDetailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryDocumentDetailRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := document.NewQueryDocumentDetailLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryDocumentDetail(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/document/querydocumentlisthandler.go b/internal/handler/app/document/querydocumentlisthandler.go new file mode 100644 index 0000000..842704a --- /dev/null +++ b/internal/handler/app/document/querydocumentlisthandler.go @@ -0,0 +1,18 @@ +package document + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/document" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get document list +func QueryDocumentListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := document.NewQueryDocumentListLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryDocumentList() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/node/getNodeListHandler.go b/internal/handler/app/node/getNodeListHandler.go new file mode 100644 index 0000000..8e0b3f0 --- /dev/null +++ b/internal/handler/app/node/getNodeListHandler.go @@ -0,0 +1,26 @@ +package node + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/node" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get Node list +func GetNodeListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.AppUserSubscbribeNodeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := node.NewGetNodeListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetNodeList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/node/getRuleGroupListHandler.go b/internal/handler/app/node/getRuleGroupListHandler.go new file mode 100644 index 0000000..1b08f0a --- /dev/null +++ b/internal/handler/app/node/getRuleGroupListHandler.go @@ -0,0 +1,18 @@ +package node + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/node" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get rule group list +func GetRuleGroupListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := node.NewGetRuleGroupListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetRuleGroupList() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/order/checkoutorderhandler.go b/internal/handler/app/order/checkoutorderhandler.go new file mode 100644 index 0000000..ec23ebc --- /dev/null +++ b/internal/handler/app/order/checkoutorderhandler.go @@ -0,0 +1,26 @@ +package order + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/order" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Checkout order +func CheckoutOrderHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CheckoutOrderRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := order.NewCheckoutOrderLogic(c.Request.Context(), svcCtx) + resp, err := l.CheckoutOrder(&req, c.Request.Host) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/order/closeorderhandler.go b/internal/handler/app/order/closeorderhandler.go new file mode 100644 index 0000000..474cfdc --- /dev/null +++ b/internal/handler/app/order/closeorderhandler.go @@ -0,0 +1,26 @@ +package order + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/order" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Close order +func CloseOrderHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CloseOrderRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := order.NewCloseOrderLogic(c.Request.Context(), svcCtx) + err := l.CloseOrder(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/app/order/precreateorderhandler.go b/internal/handler/app/order/precreateorderhandler.go new file mode 100644 index 0000000..e7a3b63 --- /dev/null +++ b/internal/handler/app/order/precreateorderhandler.go @@ -0,0 +1,26 @@ +package order + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/order" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Pre create order +func PreCreateOrderHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.PurchaseOrderRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := order.NewPreCreateOrderLogic(c.Request.Context(), svcCtx) + resp, err := l.PreCreateOrder(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/order/purchasehandler.go b/internal/handler/app/order/purchasehandler.go new file mode 100644 index 0000000..99e3d45 --- /dev/null +++ b/internal/handler/app/order/purchasehandler.go @@ -0,0 +1,26 @@ +package order + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/order" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// purchase Subscription +func PurchaseHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.PurchaseOrderRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := order.NewPurchaseLogic(c.Request.Context(), svcCtx) + resp, err := l.Purchase(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/order/queryorderdetailhandler.go b/internal/handler/app/order/queryorderdetailhandler.go new file mode 100644 index 0000000..3adc5b0 --- /dev/null +++ b/internal/handler/app/order/queryorderdetailhandler.go @@ -0,0 +1,26 @@ +package order + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/order" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get order +func QueryOrderDetailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryOrderDetailRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := order.NewQueryOrderDetailLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryOrderDetail(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/order/queryorderlisthandler.go b/internal/handler/app/order/queryorderlisthandler.go new file mode 100644 index 0000000..7a37db3 --- /dev/null +++ b/internal/handler/app/order/queryorderlisthandler.go @@ -0,0 +1,26 @@ +package order + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/order" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get order list +func QueryOrderListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryOrderListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := order.NewQueryOrderListLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryOrderList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/order/rechargehandler.go b/internal/handler/app/order/rechargehandler.go new file mode 100644 index 0000000..56fc11c --- /dev/null +++ b/internal/handler/app/order/rechargehandler.go @@ -0,0 +1,26 @@ +package order + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/order" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Recharge +func RechargeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.RechargeOrderRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := order.NewRechargeLogic(c.Request.Context(), svcCtx) + resp, err := l.Recharge(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/order/renewalhandler.go b/internal/handler/app/order/renewalhandler.go new file mode 100644 index 0000000..9482181 --- /dev/null +++ b/internal/handler/app/order/renewalhandler.go @@ -0,0 +1,26 @@ +package order + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/order" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Renewal Subscription +func RenewalHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.RenewalOrderRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := order.NewRenewalLogic(c.Request.Context(), svcCtx) + resp, err := l.Renewal(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/order/resettraffichandler.go b/internal/handler/app/order/resettraffichandler.go new file mode 100644 index 0000000..bdffad3 --- /dev/null +++ b/internal/handler/app/order/resettraffichandler.go @@ -0,0 +1,26 @@ +package order + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/order" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Reset traffic +func ResetTrafficHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.ResetTrafficOrderRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := order.NewResetTrafficLogic(c.Request.Context(), svcCtx) + resp, err := l.ResetTraffic(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/payment/getavailablepaymentmethodshandler.go b/internal/handler/app/payment/getavailablepaymentmethodshandler.go new file mode 100644 index 0000000..57e7495 --- /dev/null +++ b/internal/handler/app/payment/getavailablepaymentmethodshandler.go @@ -0,0 +1,18 @@ +package payment + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/payment" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get available payment methods +func GetAvailablePaymentMethodsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := payment.NewGetAvailablePaymentMethodsLogic(c.Request.Context(), svcCtx) + resp, err := l.GetAvailablePaymentMethods() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/subscribe/queryApplicationConfigHandler.go b/internal/handler/app/subscribe/queryApplicationConfigHandler.go new file mode 100644 index 0000000..f663047 --- /dev/null +++ b/internal/handler/app/subscribe/queryApplicationConfigHandler.go @@ -0,0 +1,18 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get application config +func QueryApplicationConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := subscribe.NewQueryApplicationConfigLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryApplicationConfig() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/subscribe/querySubscribeGroupListHandler.go b/internal/handler/app/subscribe/querySubscribeGroupListHandler.go new file mode 100644 index 0000000..8403fe8 --- /dev/null +++ b/internal/handler/app/subscribe/querySubscribeGroupListHandler.go @@ -0,0 +1,18 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get subscribe group list +func QuerySubscribeGroupListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := subscribe.NewQuerySubscribeGroupListLogic(c.Request.Context(), svcCtx) + resp, err := l.QuerySubscribeGroupList() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/subscribe/querySubscribeListHandler.go b/internal/handler/app/subscribe/querySubscribeListHandler.go new file mode 100644 index 0000000..d9f427d --- /dev/null +++ b/internal/handler/app/subscribe/querySubscribeListHandler.go @@ -0,0 +1,18 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get subscribe list +func QuerySubscribeListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := subscribe.NewQuerySubscribeListLogic(c.Request.Context(), svcCtx) + resp, err := l.QuerySubscribeList() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/subscribe/queryUserAlreadySubscribeHandler.go b/internal/handler/app/subscribe/queryUserAlreadySubscribeHandler.go new file mode 100644 index 0000000..b5324dd --- /dev/null +++ b/internal/handler/app/subscribe/queryUserAlreadySubscribeHandler.go @@ -0,0 +1,18 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get Already subscribed to package +func QueryUserAlreadySubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := subscribe.NewQueryUserAlreadySubscribeLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryUserAlreadySubscribe() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/subscribe/queryUserAvailableUserSubscribeHandler.go b/internal/handler/app/subscribe/queryUserAvailableUserSubscribeHandler.go new file mode 100644 index 0000000..9f87808 --- /dev/null +++ b/internal/handler/app/subscribe/queryUserAvailableUserSubscribeHandler.go @@ -0,0 +1,26 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get Available subscriptions for users +func QueryUserAvailableUserSubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.AppUserSubscribeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := subscribe.NewQueryUserAvailableUserSubscribeLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryUserAvailableUserSubscribe(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/subscribe/resetUserSubscribePeriodHandler.go b/internal/handler/app/subscribe/resetUserSubscribePeriodHandler.go new file mode 100644 index 0000000..fa687b9 --- /dev/null +++ b/internal/handler/app/subscribe/resetUserSubscribePeriodHandler.go @@ -0,0 +1,26 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Reset user subscription period +func ResetUserSubscribePeriodHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UserSubscribeResetPeriodRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := subscribe.NewResetUserSubscribePeriodLogic(c.Request.Context(), svcCtx) + resp, err := l.ResetUserSubscribePeriod(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/user/deleteAccountHandler.go b/internal/handler/app/user/deleteAccountHandler.go new file mode 100644 index 0000000..b7d5337 --- /dev/null +++ b/internal/handler/app/user/deleteAccountHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Delete Account +func DeleteAccountHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeleteAccountRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewDeleteAccountLogic(c.Request.Context(), svcCtx) + err := l.DeleteAccount(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/app/user/getuseronlinetimestatisticshandler.go b/internal/handler/app/user/getuseronlinetimestatisticshandler.go new file mode 100644 index 0000000..678ffa5 --- /dev/null +++ b/internal/handler/app/user/getuseronlinetimestatisticshandler.go @@ -0,0 +1,18 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get user online time total +func GetUserOnlineTimeStatisticsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := user.NewGetUserOnlineTimeStatisticsLogic(c.Request.Context(), svcCtx) + resp, err := l.GetUserOnlineTimeStatistics() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/user/getusersubscribetrafficlogshandler.go b/internal/handler/app/user/getusersubscribetrafficlogshandler.go new file mode 100644 index 0000000..b1cc669 --- /dev/null +++ b/internal/handler/app/user/getusersubscribetrafficlogshandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get user subcribe traffic logs +func GetUserSubscribeTrafficLogsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetUserSubscribeTrafficLogsRequest + _ = c.BindQuery(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewGetUserSubscribeTrafficLogsLogic(c.Request.Context(), svcCtx) + resp, err := l.GetUserSubscribeTrafficLogs(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/user/queryUserInfoHandler.go b/internal/handler/app/user/queryUserInfoHandler.go new file mode 100644 index 0000000..c955983 --- /dev/null +++ b/internal/handler/app/user/queryUserInfoHandler.go @@ -0,0 +1,18 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// query user info +func QueryUserInfoHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := user.NewQueryUserInfoLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryUserInfo() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/user/queryuseraffiliatehandler.go b/internal/handler/app/user/queryuseraffiliatehandler.go new file mode 100644 index 0000000..c5bfd2f --- /dev/null +++ b/internal/handler/app/user/queryuseraffiliatehandler.go @@ -0,0 +1,18 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Query User Affiliate Count +func QueryUserAffiliateHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := user.NewQueryUserAffiliateLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryUserAffiliate() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/user/queryuseraffiliatelisthandler.go b/internal/handler/app/user/queryuseraffiliatelisthandler.go new file mode 100644 index 0000000..6e2f6a5 --- /dev/null +++ b/internal/handler/app/user/queryuseraffiliatelisthandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Query User Affiliate List +func QueryUserAffiliateListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryUserAffiliateListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewQueryUserAffiliateListLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryUserAffiliateList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/user/updatePasswordHandler.go b/internal/handler/app/user/updatePasswordHandler.go new file mode 100644 index 0000000..532612e --- /dev/null +++ b/internal/handler/app/user/updatePasswordHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update Password +func UpdatePasswordHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdatePasswordRequeset + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewUpdatePasswordLogic(c.Request.Context(), svcCtx) + err := l.UpdatePassword(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/app/ws/appWsHandler.go b/internal/handler/app/ws/appWsHandler.go new file mode 100644 index 0000000..6e90b67 --- /dev/null +++ b/internal/handler/app/ws/appWsHandler.go @@ -0,0 +1,20 @@ +package ws + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/app/ws" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// App heartbeat +func AppWsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + ctx := c.Request.Context() + + // Logic: App heartbeat + l := ws.NewAppWsLogic(ctx, svcCtx) + err := l.AppWs(c.Writer, c.Request, c.Param("userid"), c.Param("identifier")) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/auth/checkUserHandler.go b/internal/handler/auth/checkUserHandler.go new file mode 100644 index 0000000..840642b --- /dev/null +++ b/internal/handler/auth/checkUserHandler.go @@ -0,0 +1,26 @@ +package auth + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/auth" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Check user is exist +func CheckUserHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CheckUserRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := auth.NewCheckUserLogic(c.Request.Context(), svcCtx) + resp, err := l.CheckUser(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/auth/checkUserTelephoneHandler.go b/internal/handler/auth/checkUserTelephoneHandler.go new file mode 100644 index 0000000..c438b84 --- /dev/null +++ b/internal/handler/auth/checkUserTelephoneHandler.go @@ -0,0 +1,26 @@ +package auth + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/auth" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Check user telephone is exist +func CheckUserTelephoneHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.TelephoneCheckUserRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := auth.NewCheckUserTelephoneLogic(c.Request.Context(), svcCtx) + resp, err := l.CheckUserTelephone(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/auth/oauth/appleLoginCallbackHandler.go b/internal/handler/auth/oauth/appleLoginCallbackHandler.go new file mode 100644 index 0000000..e69764d --- /dev/null +++ b/internal/handler/auth/oauth/appleLoginCallbackHandler.go @@ -0,0 +1,27 @@ +package oauth + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/auth/oauth" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Apple Login Callback +func AppleLoginCallbackHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.AppleLoginCallbackRequest + if err := c.ShouldBind(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request data"}) + return + } + l := oauth.NewAppleLoginCallbackLogic(c, svcCtx) + err := l.AppleLoginCallback(&req, c.Request, c.Writer) + if err != nil { + result.HttpResult(c, nil, err) + } + } +} diff --git a/internal/handler/auth/oauth/oAuthLoginGetTokenHandler.go b/internal/handler/auth/oauth/oAuthLoginGetTokenHandler.go new file mode 100644 index 0000000..384dafb --- /dev/null +++ b/internal/handler/auth/oauth/oAuthLoginGetTokenHandler.go @@ -0,0 +1,26 @@ +package oauth + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/auth/oauth" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// OAuth login get token +func OAuthLoginGetTokenHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.OAuthLoginGetTokenRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := oauth.NewOAuthLoginGetTokenLogic(c.Request.Context(), svcCtx) + resp, err := l.OAuthLoginGetToken(&req, c.ClientIP(), c.Request.UserAgent()) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/auth/oauth/oAuthLoginHandler.go b/internal/handler/auth/oauth/oAuthLoginHandler.go new file mode 100644 index 0000000..1653f3b --- /dev/null +++ b/internal/handler/auth/oauth/oAuthLoginHandler.go @@ -0,0 +1,26 @@ +package oauth + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/auth/oauth" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// OAuth login +func OAuthLoginHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.OAthLoginRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := oauth.NewOAuthLoginLogic(c.Request.Context(), svcCtx) + resp, err := l.OAuthLogin(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/auth/resetPasswordHandler.go b/internal/handler/auth/resetPasswordHandler.go new file mode 100644 index 0000000..73848b2 --- /dev/null +++ b/internal/handler/auth/resetPasswordHandler.go @@ -0,0 +1,43 @@ +package auth + +import ( + "time" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/auth" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/ppanel-server/pkg/turnstile" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +// Reset password +func ResetPasswordHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.ResetPasswordRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + // get client ip + req.IP = c.ClientIP() + if svcCtx.Config.Verify.ResetPasswordVerify { + verifyTurns := turnstile.New(turnstile.Config{ + Secret: svcCtx.Config.Verify.TurnstileSecret, + Timeout: 3 * time.Second, + }) + if verify, err := verifyTurns.Verify(c, req.CfToken, req.IP); err != nil || !verify { + err = errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "error: %v, verify: %v", err, verify) + result.HttpResult(c, nil, err) + return + } + } + l := auth.NewResetPasswordLogic(c.Request.Context(), svcCtx) + resp, err := l.ResetPassword(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/auth/telephoneLoginHandler.go b/internal/handler/auth/telephoneLoginHandler.go new file mode 100644 index 0000000..798b1cd --- /dev/null +++ b/internal/handler/auth/telephoneLoginHandler.go @@ -0,0 +1,43 @@ +package auth + +import ( + "time" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/auth" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/ppanel-server/pkg/turnstile" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +// User Telephone login +func TelephoneLoginHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.TelephoneLoginRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + // get client ip + req.IP = c.ClientIP() + if svcCtx.Config.Verify.LoginVerify { + verifyTurns := turnstile.New(turnstile.Config{ + Secret: svcCtx.Config.Verify.TurnstileSecret, + Timeout: 3 * time.Second, + }) + if verify, err := verifyTurns.Verify(c, req.CfToken, req.IP); err != nil || !verify { + err = errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "error: %v, verify: %v", err, verify) + result.HttpResult(c, nil, err) + return + } + } + l := auth.NewTelephoneLoginLogic(c, svcCtx) + resp, err := l.TelephoneLogin(&req, c.Request, c.ClientIP()) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/auth/telephoneResetPasswordHandler.go b/internal/handler/auth/telephoneResetPasswordHandler.go new file mode 100644 index 0000000..4b870c0 --- /dev/null +++ b/internal/handler/auth/telephoneResetPasswordHandler.go @@ -0,0 +1,43 @@ +package auth + +import ( + "time" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/auth" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/ppanel-server/pkg/turnstile" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +// Reset password +func TelephoneResetPasswordHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.TelephoneResetPasswordRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + // get client ip + req.IP = c.ClientIP() + if svcCtx.Config.Verify.ResetPasswordVerify { + verifyTurns := turnstile.New(turnstile.Config{ + Secret: svcCtx.Config.Verify.TurnstileSecret, + Timeout: 3 * time.Second, + }) + if verify, err := verifyTurns.Verify(c.Request.Context(), req.CfToken, req.IP); err != nil || !verify { + err = errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "error: %v, verify: %v", err, verify) + result.HttpResult(c, nil, err) + return + } + } + l := auth.NewTelephoneResetPasswordLogic(c, svcCtx) + resp, err := l.TelephoneResetPassword(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/auth/telephoneUserRegisterHandler.go b/internal/handler/auth/telephoneUserRegisterHandler.go new file mode 100644 index 0000000..bcaef82 --- /dev/null +++ b/internal/handler/auth/telephoneUserRegisterHandler.go @@ -0,0 +1,43 @@ +package auth + +import ( + "time" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/auth" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/ppanel-server/pkg/turnstile" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +// User Telephone register +func TelephoneUserRegisterHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.TelephoneRegisterRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + // get client ip + req.IP = c.ClientIP() + if svcCtx.Config.Verify.RegisterVerify { + verifyTurns := turnstile.New(turnstile.Config{ + Secret: svcCtx.Config.Verify.TurnstileSecret, + Timeout: 3 * time.Second, + }) + if verify, err := verifyTurns.Verify(c, req.CfToken, req.IP); err != nil || !verify { + err = errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "error: %v, verify: %v", err, verify) + result.HttpResult(c, nil, err) + return + } + } + l := auth.NewTelephoneUserRegisterLogic(c.Request.Context(), svcCtx) + resp, err := l.TelephoneUserRegister(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/auth/userLoginHandler.go b/internal/handler/auth/userLoginHandler.go new file mode 100644 index 0000000..1e857c3 --- /dev/null +++ b/internal/handler/auth/userLoginHandler.go @@ -0,0 +1,45 @@ +package auth + +import ( + "time" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/auth" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/ppanel-server/pkg/turnstile" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +// User login +func UserLoginHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UserLoginRequest + _ = c.ShouldBind(&req) + // get client ip + req.IP = c.ClientIP() + req.UserAgent = c.Request.UserAgent() + if svcCtx.Config.Verify.LoginVerify && !svcCtx.Config.Debug { + verifyTurns := turnstile.New(turnstile.Config{ + Secret: svcCtx.Config.Verify.TurnstileSecret, + Timeout: 3 * time.Second, + }) + if verify, err := verifyTurns.Verify(c, req.CfToken, req.IP); err != nil || !verify { + err = errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "error: %v, verify: %v", err, verify) + result.HttpResult(c, nil, err) + return + } + } + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := auth.NewUserLoginLogic(c.Request.Context(), svcCtx) + resp, err := l.UserLogin(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/auth/userRegisterHandler.go b/internal/handler/auth/userRegisterHandler.go new file mode 100644 index 0000000..9bdcd99 --- /dev/null +++ b/internal/handler/auth/userRegisterHandler.go @@ -0,0 +1,43 @@ +package auth + +import ( + "time" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/auth" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/ppanel-server/pkg/turnstile" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +// User register +func UserRegisterHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UserRegisterRequest + _ = c.ShouldBind(&req) + // get client ip + req.IP = c.ClientIP() + if svcCtx.Config.Verify.RegisterVerify { + verifyTurns := turnstile.New(turnstile.Config{ + Secret: svcCtx.Config.Verify.TurnstileSecret, + Timeout: 3 * time.Second, + }) + if verify, err := verifyTurns.Verify(c, req.CfToken, req.IP); err != nil || !verify { + result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "verify error: %v", err.Error())) + return + } + } + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := auth.NewUserRegisterLogic(c.Request.Context(), svcCtx) + resp, err := l.UserRegister(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/common/checkverificationcodehandler.go b/internal/handler/common/checkverificationcodehandler.go new file mode 100644 index 0000000..0bdf6be --- /dev/null +++ b/internal/handler/common/checkverificationcodehandler.go @@ -0,0 +1,26 @@ +package common + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/common" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Check verification code +func CheckVerificationCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CheckVerificationCodeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := common.NewCheckVerificationCodeLogic(c.Request.Context(), svcCtx) + resp, err := l.CheckVerificationCode(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/common/getAdsHandler.go b/internal/handler/common/getAdsHandler.go new file mode 100644 index 0000000..5eba200 --- /dev/null +++ b/internal/handler/common/getAdsHandler.go @@ -0,0 +1,26 @@ +package common + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/common" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get Ads +func GetAdsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetAdsRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := common.NewGetAdsLogic(c.Request.Context(), svcCtx) + resp, err := l.GetAds(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/common/getApplicationHandler.go b/internal/handler/common/getApplicationHandler.go new file mode 100644 index 0000000..fa91a77 --- /dev/null +++ b/internal/handler/common/getApplicationHandler.go @@ -0,0 +1,18 @@ +package common + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/common" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get Tos Content +func GetApplicationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := common.NewGetApplicationLogic(c.Request.Context(), svcCtx) + resp, err := l.GetApplication() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/common/getGlobalConfigHandler.go b/internal/handler/common/getGlobalConfigHandler.go new file mode 100644 index 0000000..0d45405 --- /dev/null +++ b/internal/handler/common/getGlobalConfigHandler.go @@ -0,0 +1,17 @@ +package common + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/common" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get global config +func GetGlobalConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + l := common.NewGetGlobalConfigLogic(c.Request.Context(), svcCtx) + resp, err := l.GetGlobalConfig() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/common/getPrivacyPolicyHandler.go b/internal/handler/common/getPrivacyPolicyHandler.go new file mode 100644 index 0000000..09f6c6e --- /dev/null +++ b/internal/handler/common/getPrivacyPolicyHandler.go @@ -0,0 +1,18 @@ +package common + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/common" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get Privacy Policy +func GetPrivacyPolicyHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := common.NewGetPrivacyPolicyLogic(c.Request.Context(), svcCtx) + resp, err := l.GetPrivacyPolicy() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/common/getStatHandler.go b/internal/handler/common/getStatHandler.go new file mode 100644 index 0000000..a370fd3 --- /dev/null +++ b/internal/handler/common/getStatHandler.go @@ -0,0 +1,18 @@ +package common + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/common" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get stat +func GetStatHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := common.NewGetStatLogic(c.Request.Context(), svcCtx) + resp, err := l.GetStat() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/common/getSubscriptionHandler.go b/internal/handler/common/getSubscriptionHandler.go new file mode 100644 index 0000000..a63e221 --- /dev/null +++ b/internal/handler/common/getSubscriptionHandler.go @@ -0,0 +1,18 @@ +package common + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/common" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get Subscription +func GetSubscriptionHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := common.NewGetSubscriptionLogic(c.Request.Context(), svcCtx) + resp, err := l.GetSubscription() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/common/getTosHandler.go b/internal/handler/common/getTosHandler.go new file mode 100644 index 0000000..2979881 --- /dev/null +++ b/internal/handler/common/getTosHandler.go @@ -0,0 +1,18 @@ +package common + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/common" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get Tos Content +func GetTosHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := common.NewGetTosLogic(c.Request.Context(), svcCtx) + resp, err := l.GetTos() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/common/sendEmailCodeHandler.go b/internal/handler/common/sendEmailCodeHandler.go new file mode 100644 index 0000000..7c9d372 --- /dev/null +++ b/internal/handler/common/sendEmailCodeHandler.go @@ -0,0 +1,26 @@ +package common + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/common" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get verification code +func SendEmailCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.SendCodeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := common.NewSendEmailCodeLogic(c.Request.Context(), svcCtx) + resp, err := l.SendEmailCode(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/common/sendSmsCodeHandler.go b/internal/handler/common/sendSmsCodeHandler.go new file mode 100644 index 0000000..2b7220d --- /dev/null +++ b/internal/handler/common/sendSmsCodeHandler.go @@ -0,0 +1,26 @@ +package common + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/common" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get sms verification code +func SendSmsCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.SendSmsCodeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := common.NewSendSmsCodeLogic(c.Request.Context(), svcCtx) + resp, err := l.SendSmsCode(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/notify.go b/internal/handler/notify.go new file mode 100644 index 0000000..a73bd70 --- /dev/null +++ b/internal/handler/notify.go @@ -0,0 +1,17 @@ +package handler + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/handler/notify" + "github.com/perfect-panel/ppanel-server/internal/middleware" + "github.com/perfect-panel/ppanel-server/internal/svc" +) + +func RegisterNotifyHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { + group := router.Group("/v1/notify/") + group.Use(middleware.NotifyMiddleware(serverCtx)) + { + group.Any("/:platform/:token", notify.PaymentNotifyHandler(serverCtx)) + } + +} diff --git a/internal/handler/notify/paymentNotifyHandler.go b/internal/handler/notify/paymentNotifyHandler.go new file mode 100644 index 0000000..f84ad00 --- /dev/null +++ b/internal/handler/notify/paymentNotifyHandler.go @@ -0,0 +1,63 @@ +package notify + +import ( + "fmt" + "net/http" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/notify" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/payment" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// PaymentNotifyHandler Payment Notify +func PaymentNotifyHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + platform, ok := c.Request.Context().Value(constant.CtxKeyPlatform).(string) + if !ok { + logger.WithContext(c.Request.Context()).Errorf("platform not found") + result.HttpResult(c, nil, fmt.Errorf("platform not found")) + return + } + + switch payment.ParsePlatform(platform) { + case payment.EPay: + req := &types.EPayNotifyRequest{} + if err := c.ShouldBind(req); err != nil { + result.HttpResult(c, nil, err) + return + } + l := notify.NewEPayNotifyLogic(c, svcCtx) + if err := l.EPayNotify(req); err != nil { + logger.WithContext(c.Request.Context()).Errorf("EPayNotify failed: %v", err.Error()) + c.String(http.StatusBadRequest, err.Error()) + return + } + c.String(http.StatusOK, "%s", "success") + case payment.Stripe: + l := notify.NewStripeNotifyLogic(c.Request.Context(), svcCtx) + if err := l.StripeNotify(c.Request, c.Writer); err != nil { + result.HttpResult(c, nil, err) + return + } + result.HttpResult(c, nil, nil) + + case payment.AlipayF2F: + l := notify.NewAlipayNotifyLogic(c.Request.Context(), svcCtx) + if err := l.AlipayNotify(c.Request); err != nil { + result.HttpResult(c, nil, err) + return + } + // Return success to alipay + c.String(http.StatusOK, "%s", "success") + + default: + logger.WithContext(c.Request.Context()).Errorf("platform %s not support", platform) + } + } +} diff --git a/internal/handler/public/announcement/queryAnnouncementHandler.go b/internal/handler/public/announcement/queryAnnouncementHandler.go new file mode 100644 index 0000000..dd2a8e8 --- /dev/null +++ b/internal/handler/public/announcement/queryAnnouncementHandler.go @@ -0,0 +1,26 @@ +package announcement + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/announcement" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Query announcement +func QueryAnnouncementHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryAnnouncementRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := announcement.NewQueryAnnouncementLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryAnnouncement(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/document/queryDocumentDetailHandler.go b/internal/handler/public/document/queryDocumentDetailHandler.go new file mode 100644 index 0000000..7b2524b --- /dev/null +++ b/internal/handler/public/document/queryDocumentDetailHandler.go @@ -0,0 +1,26 @@ +package document + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/document" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get document detail +func QueryDocumentDetailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryDocumentDetailRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := document.NewQueryDocumentDetailLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryDocumentDetail(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/document/queryDocumentListHandler.go b/internal/handler/public/document/queryDocumentListHandler.go new file mode 100644 index 0000000..9a57040 --- /dev/null +++ b/internal/handler/public/document/queryDocumentListHandler.go @@ -0,0 +1,18 @@ +package document + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/document" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get document list +func QueryDocumentListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := document.NewQueryDocumentListLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryDocumentList() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/order/closeOrderHandler.go b/internal/handler/public/order/closeOrderHandler.go new file mode 100644 index 0000000..c0c9120 --- /dev/null +++ b/internal/handler/public/order/closeOrderHandler.go @@ -0,0 +1,26 @@ +package order + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/order" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Close order +func CloseOrderHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CloseOrderRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := order.NewCloseOrderLogic(c.Request.Context(), svcCtx) + err := l.CloseOrder(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/public/order/preCreateOrderHandler.go b/internal/handler/public/order/preCreateOrderHandler.go new file mode 100644 index 0000000..bca9773 --- /dev/null +++ b/internal/handler/public/order/preCreateOrderHandler.go @@ -0,0 +1,26 @@ +package order + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/order" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Pre create order +func PreCreateOrderHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.PurchaseOrderRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := order.NewPreCreateOrderLogic(c.Request.Context(), svcCtx) + resp, err := l.PreCreateOrder(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/order/purchaseHandler.go b/internal/handler/public/order/purchaseHandler.go new file mode 100644 index 0000000..a7eb606 --- /dev/null +++ b/internal/handler/public/order/purchaseHandler.go @@ -0,0 +1,26 @@ +package order + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/order" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// purchase Subscription +func PurchaseHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.PurchaseOrderRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := order.NewPurchaseLogic(c.Request.Context(), svcCtx) + resp, err := l.Purchase(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/order/queryOrderDetailHandler.go b/internal/handler/public/order/queryOrderDetailHandler.go new file mode 100644 index 0000000..dcc3b9f --- /dev/null +++ b/internal/handler/public/order/queryOrderDetailHandler.go @@ -0,0 +1,26 @@ +package order + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/order" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get order +func QueryOrderDetailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryOrderDetailRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := order.NewQueryOrderDetailLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryOrderDetail(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/order/queryOrderListHandler.go b/internal/handler/public/order/queryOrderListHandler.go new file mode 100644 index 0000000..4ecf7ce --- /dev/null +++ b/internal/handler/public/order/queryOrderListHandler.go @@ -0,0 +1,26 @@ +package order + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/order" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get order list +func QueryOrderListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryOrderListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := order.NewQueryOrderListLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryOrderList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/order/rechargeHandler.go b/internal/handler/public/order/rechargeHandler.go new file mode 100644 index 0000000..3807585 --- /dev/null +++ b/internal/handler/public/order/rechargeHandler.go @@ -0,0 +1,26 @@ +package order + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/order" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Recharge +func RechargeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.RechargeOrderRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := order.NewRechargeLogic(c.Request.Context(), svcCtx) + resp, err := l.Recharge(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/order/renewalHandler.go b/internal/handler/public/order/renewalHandler.go new file mode 100644 index 0000000..8cd6c5a --- /dev/null +++ b/internal/handler/public/order/renewalHandler.go @@ -0,0 +1,26 @@ +package order + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/order" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Renewal Subscription +func RenewalHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.RenewalOrderRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := order.NewRenewalLogic(c.Request.Context(), svcCtx) + resp, err := l.Renewal(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/order/resetTrafficHandler.go b/internal/handler/public/order/resetTrafficHandler.go new file mode 100644 index 0000000..d4cb5af --- /dev/null +++ b/internal/handler/public/order/resetTrafficHandler.go @@ -0,0 +1,26 @@ +package order + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/order" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Reset traffic +func ResetTrafficHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.ResetTrafficOrderRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := order.NewResetTrafficLogic(c.Request.Context(), svcCtx) + resp, err := l.ResetTraffic(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/payment/getAvailablePaymentMethodsHandler.go b/internal/handler/public/payment/getAvailablePaymentMethodsHandler.go new file mode 100644 index 0000000..96aa03d --- /dev/null +++ b/internal/handler/public/payment/getAvailablePaymentMethodsHandler.go @@ -0,0 +1,18 @@ +package payment + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/payment" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get available payment methods +func GetAvailablePaymentMethodsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := payment.NewGetAvailablePaymentMethodsLogic(c.Request.Context(), svcCtx) + resp, err := l.GetAvailablePaymentMethods() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/portal/getAvailablePaymentMethodsHandler.go b/internal/handler/public/portal/getAvailablePaymentMethodsHandler.go new file mode 100644 index 0000000..cd6a19a --- /dev/null +++ b/internal/handler/public/portal/getAvailablePaymentMethodsHandler.go @@ -0,0 +1,18 @@ +package portal + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/portal" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get available payment methods +func GetAvailablePaymentMethodsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := portal.NewGetAvailablePaymentMethodsLogic(c.Request.Context(), svcCtx) + resp, err := l.GetAvailablePaymentMethods() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/portal/getSubscriptionHandler.go b/internal/handler/public/portal/getSubscriptionHandler.go new file mode 100644 index 0000000..695ceba --- /dev/null +++ b/internal/handler/public/portal/getSubscriptionHandler.go @@ -0,0 +1,17 @@ +package portal + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/portal" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get Subscription +func GetSubscriptionHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + l := portal.NewGetSubscriptionLogic(c.Request.Context(), svcCtx) + resp, err := l.GetSubscription() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/portal/prePurchaseOrderHandler.go b/internal/handler/public/portal/prePurchaseOrderHandler.go new file mode 100644 index 0000000..37650b2 --- /dev/null +++ b/internal/handler/public/portal/prePurchaseOrderHandler.go @@ -0,0 +1,26 @@ +package portal + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/portal" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Pre Purchase Order +func PrePurchaseOrderHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.PrePurchaseOrderRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := portal.NewPrePurchaseOrderLogic(c.Request.Context(), svcCtx) + resp, err := l.PrePurchaseOrder(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/portal/purchaseCheckoutHandler.go b/internal/handler/public/portal/purchaseCheckoutHandler.go new file mode 100644 index 0000000..ec06c9e --- /dev/null +++ b/internal/handler/public/portal/purchaseCheckoutHandler.go @@ -0,0 +1,26 @@ +package portal + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/portal" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Purchase Checkout +func PurchaseCheckoutHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CheckoutOrderRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := portal.NewPurchaseCheckoutLogic(c.Request.Context(), svcCtx) + resp, err := l.PurchaseCheckout(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/portal/purchaseHandler.go b/internal/handler/public/portal/purchaseHandler.go new file mode 100644 index 0000000..a483050 --- /dev/null +++ b/internal/handler/public/portal/purchaseHandler.go @@ -0,0 +1,26 @@ +package portal + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/portal" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Purchase subscription +func PurchaseHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.PortalPurchaseRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := portal.NewPurchaseLogic(c.Request.Context(), svcCtx) + resp, err := l.Purchase(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/portal/queryPurchaseOrderHandler.go b/internal/handler/public/portal/queryPurchaseOrderHandler.go new file mode 100644 index 0000000..2e7cf2e --- /dev/null +++ b/internal/handler/public/portal/queryPurchaseOrderHandler.go @@ -0,0 +1,26 @@ +package portal + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/portal" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Query Purchase Order +func QueryPurchaseOrderHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryPurchaseOrderRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := portal.NewQueryPurchaseOrderLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryPurchaseOrder(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/subscribe/queryApplicationConfigHandler.go b/internal/handler/public/subscribe/queryApplicationConfigHandler.go new file mode 100644 index 0000000..7cb91c5 --- /dev/null +++ b/internal/handler/public/subscribe/queryApplicationConfigHandler.go @@ -0,0 +1,18 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get application config +func QueryApplicationConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := subscribe.NewQueryApplicationConfigLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryApplicationConfig() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/subscribe/querySubscribeGroupListHandler.go b/internal/handler/public/subscribe/querySubscribeGroupListHandler.go new file mode 100644 index 0000000..8b48fc1 --- /dev/null +++ b/internal/handler/public/subscribe/querySubscribeGroupListHandler.go @@ -0,0 +1,18 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get subscribe group list +func QuerySubscribeGroupListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := subscribe.NewQuerySubscribeGroupListLogic(c.Request.Context(), svcCtx) + resp, err := l.QuerySubscribeGroupList() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/subscribe/querySubscribeListHandler.go b/internal/handler/public/subscribe/querySubscribeListHandler.go new file mode 100644 index 0000000..551725d --- /dev/null +++ b/internal/handler/public/subscribe/querySubscribeListHandler.go @@ -0,0 +1,18 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get subscribe list +func QuerySubscribeListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := subscribe.NewQuerySubscribeListLogic(c.Request.Context(), svcCtx) + resp, err := l.QuerySubscribeList() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/ticket/createUserTicketFollowHandler.go b/internal/handler/public/ticket/createUserTicketFollowHandler.go new file mode 100644 index 0000000..b94334e --- /dev/null +++ b/internal/handler/public/ticket/createUserTicketFollowHandler.go @@ -0,0 +1,26 @@ +package ticket + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/ticket" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Create ticket follow +func CreateUserTicketFollowHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateUserTicketFollowRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := ticket.NewCreateUserTicketFollowLogic(c.Request.Context(), svcCtx) + err := l.CreateUserTicketFollow(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/public/ticket/createUserTicketHandler.go b/internal/handler/public/ticket/createUserTicketHandler.go new file mode 100644 index 0000000..2cbae74 --- /dev/null +++ b/internal/handler/public/ticket/createUserTicketHandler.go @@ -0,0 +1,26 @@ +package ticket + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/ticket" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Create ticket +func CreateUserTicketHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateUserTicketRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := ticket.NewCreateUserTicketLogic(c.Request.Context(), svcCtx) + err := l.CreateUserTicket(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/public/ticket/getUserTicketDetailsHandler.go b/internal/handler/public/ticket/getUserTicketDetailsHandler.go new file mode 100644 index 0000000..6230664 --- /dev/null +++ b/internal/handler/public/ticket/getUserTicketDetailsHandler.go @@ -0,0 +1,26 @@ +package ticket + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/ticket" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get ticket detail +func GetUserTicketDetailsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetUserTicketDetailRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := ticket.NewGetUserTicketDetailsLogic(c.Request.Context(), svcCtx) + resp, err := l.GetUserTicketDetails(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/ticket/getUserTicketListHandler.go b/internal/handler/public/ticket/getUserTicketListHandler.go new file mode 100644 index 0000000..9b86d0e --- /dev/null +++ b/internal/handler/public/ticket/getUserTicketListHandler.go @@ -0,0 +1,26 @@ +package ticket + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/ticket" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get ticket list +func GetUserTicketListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetUserTicketListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := ticket.NewGetUserTicketListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetUserTicketList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/ticket/updateUserTicketStatusHandler.go b/internal/handler/public/ticket/updateUserTicketStatusHandler.go new file mode 100644 index 0000000..12e7c82 --- /dev/null +++ b/internal/handler/public/ticket/updateUserTicketStatusHandler.go @@ -0,0 +1,26 @@ +package ticket + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/ticket" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update ticket status +func UpdateUserTicketStatusHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateUserTicketStatusRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := ticket.NewUpdateUserTicketStatusLogic(c.Request.Context(), svcCtx) + err := l.UpdateUserTicketStatus(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/public/user/bindOAuthCallbackHandler.go b/internal/handler/public/user/bindOAuthCallbackHandler.go new file mode 100644 index 0000000..67cc030 --- /dev/null +++ b/internal/handler/public/user/bindOAuthCallbackHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Bind OAuth Callback +func BindOAuthCallbackHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.BindOAuthCallbackRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewBindOAuthCallbackLogic(c.Request.Context(), svcCtx) + err := l.BindOAuthCallback(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/public/user/bindOAuthHandler.go b/internal/handler/public/user/bindOAuthHandler.go new file mode 100644 index 0000000..f80dfa9 --- /dev/null +++ b/internal/handler/public/user/bindOAuthHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Bind OAuth +func BindOAuthHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.BindOAuthRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewBindOAuthLogic(c.Request.Context(), svcCtx) + resp, err := l.BindOAuth(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/user/bindTelegramHandler.go b/internal/handler/public/user/bindTelegramHandler.go new file mode 100644 index 0000000..3ae8630 --- /dev/null +++ b/internal/handler/public/user/bindTelegramHandler.go @@ -0,0 +1,18 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Bind Telegram +func BindTelegramHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := user.NewBindTelegramLogic(c.Request.Context(), svcCtx) + resp, err := l.BindTelegram() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/user/getLoginLogHandler.go b/internal/handler/public/user/getLoginLogHandler.go new file mode 100644 index 0000000..9a07df0 --- /dev/null +++ b/internal/handler/public/user/getLoginLogHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get Login Log +func GetLoginLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetLoginLogRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewGetLoginLogLogic(c.Request.Context(), svcCtx) + resp, err := l.GetLoginLog(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/user/getOAuthMethodsHandler.go b/internal/handler/public/user/getOAuthMethodsHandler.go new file mode 100644 index 0000000..5e62f51 --- /dev/null +++ b/internal/handler/public/user/getOAuthMethodsHandler.go @@ -0,0 +1,18 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get OAuth Methods +func GetOAuthMethodsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := user.NewGetOAuthMethodsLogic(c.Request.Context(), svcCtx) + resp, err := l.GetOAuthMethods() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/user/getSubscribeLogHandler.go b/internal/handler/public/user/getSubscribeLogHandler.go new file mode 100644 index 0000000..eec3314 --- /dev/null +++ b/internal/handler/public/user/getSubscribeLogHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Get Subscribe Log +func GetSubscribeLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetSubscribeLogRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewGetSubscribeLogLogic(c.Request.Context(), svcCtx) + resp, err := l.GetSubscribeLog(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/user/preUnsubscribeHandler.go b/internal/handler/public/user/preUnsubscribeHandler.go new file mode 100644 index 0000000..a2f76b8 --- /dev/null +++ b/internal/handler/public/user/preUnsubscribeHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Pre Unsubscribe +func PreUnsubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.PreUnsubscribeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewPreUnsubscribeLogic(c.Request.Context(), svcCtx) + resp, err := l.PreUnsubscribe(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/user/queryUserAffiliateHandler.go b/internal/handler/public/user/queryUserAffiliateHandler.go new file mode 100644 index 0000000..79f7109 --- /dev/null +++ b/internal/handler/public/user/queryUserAffiliateHandler.go @@ -0,0 +1,18 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Query User Affiliate Count +func QueryUserAffiliateHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := user.NewQueryUserAffiliateLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryUserAffiliate() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/user/queryUserAffiliateListHandler.go b/internal/handler/public/user/queryUserAffiliateListHandler.go new file mode 100644 index 0000000..8cca91b --- /dev/null +++ b/internal/handler/public/user/queryUserAffiliateListHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Query User Affiliate List +func QueryUserAffiliateListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryUserAffiliateListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewQueryUserAffiliateListLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryUserAffiliateList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/user/queryUserBalanceLogHandler.go b/internal/handler/public/user/queryUserBalanceLogHandler.go new file mode 100644 index 0000000..f83fb9d --- /dev/null +++ b/internal/handler/public/user/queryUserBalanceLogHandler.go @@ -0,0 +1,18 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Query User Balance Log +func QueryUserBalanceLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := user.NewQueryUserBalanceLogLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryUserBalanceLog() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/user/queryUserCommissionLogHandler.go b/internal/handler/public/user/queryUserCommissionLogHandler.go new file mode 100644 index 0000000..4ca2a44 --- /dev/null +++ b/internal/handler/public/user/queryUserCommissionLogHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Query User Commission Log +func QueryUserCommissionLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryUserCommissionLogListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewQueryUserCommissionLogLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryUserCommissionLog(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/user/queryUserInfoHandler.go b/internal/handler/public/user/queryUserInfoHandler.go new file mode 100644 index 0000000..13c284d --- /dev/null +++ b/internal/handler/public/user/queryUserInfoHandler.go @@ -0,0 +1,18 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Query User Info +func QueryUserInfoHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := user.NewQueryUserInfoLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryUserInfo() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/user/queryUserSubscribeHandler.go b/internal/handler/public/user/queryUserSubscribeHandler.go new file mode 100644 index 0000000..e16aadd --- /dev/null +++ b/internal/handler/public/user/queryUserSubscribeHandler.go @@ -0,0 +1,18 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Query User Subscribe +func QueryUserSubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := user.NewQueryUserSubscribeLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryUserSubscribe() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/user/resetUserSubscribeTokenHandler.go b/internal/handler/public/user/resetUserSubscribeTokenHandler.go new file mode 100644 index 0000000..2002dc1 --- /dev/null +++ b/internal/handler/public/user/resetUserSubscribeTokenHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Reset User Subscribe Token +func ResetUserSubscribeTokenHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.ResetUserSubscribeTokenRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewResetUserSubscribeTokenLogic(c.Request.Context(), svcCtx) + err := l.ResetUserSubscribeToken(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/public/user/unbindOAuthHandler.go b/internal/handler/public/user/unbindOAuthHandler.go new file mode 100644 index 0000000..de9ed68 --- /dev/null +++ b/internal/handler/public/user/unbindOAuthHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Unbind OAuth +func UnbindOAuthHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UnbindOAuthRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewUnbindOAuthLogic(c.Request.Context(), svcCtx) + err := l.UnbindOAuth(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/public/user/unbindTelegramHandler.go b/internal/handler/public/user/unbindTelegramHandler.go new file mode 100644 index 0000000..902b58f --- /dev/null +++ b/internal/handler/public/user/unbindTelegramHandler.go @@ -0,0 +1,18 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Unbind Telegram +func UnbindTelegramHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := user.NewUnbindTelegramLogic(c.Request.Context(), svcCtx) + err := l.UnbindTelegram() + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/public/user/unsubscribeHandler.go b/internal/handler/public/user/unsubscribeHandler.go new file mode 100644 index 0000000..1ad3bb1 --- /dev/null +++ b/internal/handler/public/user/unsubscribeHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Unsubscribe +func UnsubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UnsubscribeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewUnsubscribeLogic(c.Request.Context(), svcCtx) + err := l.Unsubscribe(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/public/user/updateBindEmailHandler.go b/internal/handler/public/user/updateBindEmailHandler.go new file mode 100644 index 0000000..a00a5bc --- /dev/null +++ b/internal/handler/public/user/updateBindEmailHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update Bind Email +func UpdateBindEmailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateBindEmailRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewUpdateBindEmailLogic(c.Request.Context(), svcCtx) + err := l.UpdateBindEmail(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/public/user/updateBindMobileHandler.go b/internal/handler/public/user/updateBindMobileHandler.go new file mode 100644 index 0000000..045d152 --- /dev/null +++ b/internal/handler/public/user/updateBindMobileHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update Bind Mobile +func UpdateBindMobileHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateBindMobileRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewUpdateBindMobileLogic(c.Request.Context(), svcCtx) + err := l.UpdateBindMobile(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/public/user/updateUserNotifyHandler.go b/internal/handler/public/user/updateUserNotifyHandler.go new file mode 100644 index 0000000..7b38554 --- /dev/null +++ b/internal/handler/public/user/updateUserNotifyHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update User Notify +func UpdateUserNotifyHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateUserNotifyRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewUpdateUserNotifyLogic(c.Request.Context(), svcCtx) + err := l.UpdateUserNotify(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/public/user/updateUserPasswordHandler.go b/internal/handler/public/user/updateUserPasswordHandler.go new file mode 100644 index 0000000..3f23c8a --- /dev/null +++ b/internal/handler/public/user/updateUserPasswordHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Update User Password +func UpdateUserPasswordHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateUserPasswordRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewUpdateUserPasswordLogic(c.Request.Context(), svcCtx) + err := l.UpdateUserPassword(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/public/user/verifyEmailHandler.go b/internal/handler/public/user/verifyEmailHandler.go new file mode 100644 index 0000000..d959d70 --- /dev/null +++ b/internal/handler/public/user/verifyEmailHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/public/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Verify Email +func VerifyEmailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.VerifyEmailRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewVerifyEmailLogic(c.Request.Context(), svcCtx) + err := l.VerifyEmail(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go new file mode 100644 index 0000000..c684245 --- /dev/null +++ b/internal/handler/routes.go @@ -0,0 +1,944 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.7.2 + +package handler + +import ( + "github.com/gin-gonic/gin" + adminAds "github.com/perfect-panel/ppanel-server/internal/handler/admin/ads" + adminAnnouncement "github.com/perfect-panel/ppanel-server/internal/handler/admin/announcement" + adminAuthMethod "github.com/perfect-panel/ppanel-server/internal/handler/admin/authMethod" + adminConsole "github.com/perfect-panel/ppanel-server/internal/handler/admin/console" + adminCoupon "github.com/perfect-panel/ppanel-server/internal/handler/admin/coupon" + adminDocument "github.com/perfect-panel/ppanel-server/internal/handler/admin/document" + adminLog "github.com/perfect-panel/ppanel-server/internal/handler/admin/log" + adminOrder "github.com/perfect-panel/ppanel-server/internal/handler/admin/order" + adminPayment "github.com/perfect-panel/ppanel-server/internal/handler/admin/payment" + adminServer "github.com/perfect-panel/ppanel-server/internal/handler/admin/server" + adminSubscribe "github.com/perfect-panel/ppanel-server/internal/handler/admin/subscribe" + adminSystem "github.com/perfect-panel/ppanel-server/internal/handler/admin/system" + adminTicket "github.com/perfect-panel/ppanel-server/internal/handler/admin/ticket" + adminTool "github.com/perfect-panel/ppanel-server/internal/handler/admin/tool" + adminUser "github.com/perfect-panel/ppanel-server/internal/handler/admin/user" + appAnnouncement "github.com/perfect-panel/ppanel-server/internal/handler/app/announcement" + appAuth "github.com/perfect-panel/ppanel-server/internal/handler/app/auth" + appDocument "github.com/perfect-panel/ppanel-server/internal/handler/app/document" + appNode "github.com/perfect-panel/ppanel-server/internal/handler/app/node" + appOrder "github.com/perfect-panel/ppanel-server/internal/handler/app/order" + appPayment "github.com/perfect-panel/ppanel-server/internal/handler/app/payment" + appSubscribe "github.com/perfect-panel/ppanel-server/internal/handler/app/subscribe" + appUser "github.com/perfect-panel/ppanel-server/internal/handler/app/user" + appWs "github.com/perfect-panel/ppanel-server/internal/handler/app/ws" + auth "github.com/perfect-panel/ppanel-server/internal/handler/auth" + authOauth "github.com/perfect-panel/ppanel-server/internal/handler/auth/oauth" + common "github.com/perfect-panel/ppanel-server/internal/handler/common" + publicAnnouncement "github.com/perfect-panel/ppanel-server/internal/handler/public/announcement" + publicDocument "github.com/perfect-panel/ppanel-server/internal/handler/public/document" + publicOrder "github.com/perfect-panel/ppanel-server/internal/handler/public/order" + publicPayment "github.com/perfect-panel/ppanel-server/internal/handler/public/payment" + publicPortal "github.com/perfect-panel/ppanel-server/internal/handler/public/portal" + publicSubscribe "github.com/perfect-panel/ppanel-server/internal/handler/public/subscribe" + publicTicket "github.com/perfect-panel/ppanel-server/internal/handler/public/ticket" + publicUser "github.com/perfect-panel/ppanel-server/internal/handler/public/user" + server "github.com/perfect-panel/ppanel-server/internal/handler/server" + "github.com/perfect-panel/ppanel-server/internal/middleware" + "github.com/perfect-panel/ppanel-server/internal/svc" +) + +func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { + adminAdsGroupRouter := router.Group("/v1/admin/ads") + adminAdsGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Create Ads + adminAdsGroupRouter.POST("/", adminAds.CreateAdsHandler(serverCtx)) + + // Update Ads + adminAdsGroupRouter.PUT("/", adminAds.UpdateAdsHandler(serverCtx)) + + // Delete Ads + adminAdsGroupRouter.DELETE("/", adminAds.DeleteAdsHandler(serverCtx)) + + // Get Ads Detail + adminAdsGroupRouter.GET("/detail", adminAds.GetAdsDetailHandler(serverCtx)) + + // Get Ads List + adminAdsGroupRouter.GET("/list", adminAds.GetAdsListHandler(serverCtx)) + } + + adminAnnouncementGroupRouter := router.Group("/v1/admin/announcement") + adminAnnouncementGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Create announcement + adminAnnouncementGroupRouter.POST("/", adminAnnouncement.CreateAnnouncementHandler(serverCtx)) + + // Update announcement + adminAnnouncementGroupRouter.PUT("/", adminAnnouncement.UpdateAnnouncementHandler(serverCtx)) + + // Delete announcement + adminAnnouncementGroupRouter.DELETE("/", adminAnnouncement.DeleteAnnouncementHandler(serverCtx)) + + // Get announcement + adminAnnouncementGroupRouter.GET("/detail", adminAnnouncement.GetAnnouncementHandler(serverCtx)) + + // Get announcement list + adminAnnouncementGroupRouter.GET("/list", adminAnnouncement.GetAnnouncementListHandler(serverCtx)) + } + + adminAuthMethodGroupRouter := router.Group("/v1/admin/auth-method") + adminAuthMethodGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Get auth method config + adminAuthMethodGroupRouter.GET("/config", adminAuthMethod.GetAuthMethodConfigHandler(serverCtx)) + + // Update auth method config + adminAuthMethodGroupRouter.PUT("/config", adminAuthMethod.UpdateAuthMethodConfigHandler(serverCtx)) + + // Get email support platform + adminAuthMethodGroupRouter.GET("/email_platform", adminAuthMethod.GetEmailPlatformHandler(serverCtx)) + + // Get auth method list + adminAuthMethodGroupRouter.GET("/list", adminAuthMethod.GetAuthMethodListHandler(serverCtx)) + + // Get sms support platform + adminAuthMethodGroupRouter.GET("/sms_platform", adminAuthMethod.GetSmsPlatformHandler(serverCtx)) + + // Test email send + adminAuthMethodGroupRouter.POST("/test_email_send", adminAuthMethod.TestEmailSendHandler(serverCtx)) + + // Test sms send + adminAuthMethodGroupRouter.POST("/test_sms_send", adminAuthMethod.TestSmsSendHandler(serverCtx)) + } + + adminConsoleGroupRouter := router.Group("/v1/admin/console") + adminConsoleGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Query revenue statistics + adminConsoleGroupRouter.GET("/revenue", adminConsole.QueryRevenueStatisticsHandler(serverCtx)) + + // Query server total data + adminConsoleGroupRouter.GET("/server", adminConsole.QueryServerTotalDataHandler(serverCtx)) + + // Query ticket wait reply + adminConsoleGroupRouter.GET("/ticket", adminConsole.QueryTicketWaitReplyHandler(serverCtx)) + + // Query user statistics + adminConsoleGroupRouter.GET("/user", adminConsole.QueryUserStatisticsHandler(serverCtx)) + } + + adminCouponGroupRouter := router.Group("/v1/admin/coupon") + adminCouponGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Create coupon + adminCouponGroupRouter.POST("/", adminCoupon.CreateCouponHandler(serverCtx)) + + // Update coupon + adminCouponGroupRouter.PUT("/", adminCoupon.UpdateCouponHandler(serverCtx)) + + // Delete coupon + adminCouponGroupRouter.DELETE("/", adminCoupon.DeleteCouponHandler(serverCtx)) + + // Batch delete coupon + adminCouponGroupRouter.DELETE("/batch", adminCoupon.BatchDeleteCouponHandler(serverCtx)) + + // Get coupon list + adminCouponGroupRouter.GET("/list", adminCoupon.GetCouponListHandler(serverCtx)) + } + + adminDocumentGroupRouter := router.Group("/v1/admin/document") + adminDocumentGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Create document + adminDocumentGroupRouter.POST("/", adminDocument.CreateDocumentHandler(serverCtx)) + + // Update document + adminDocumentGroupRouter.PUT("/", adminDocument.UpdateDocumentHandler(serverCtx)) + + // Delete document + adminDocumentGroupRouter.DELETE("/", adminDocument.DeleteDocumentHandler(serverCtx)) + + // Batch delete document + adminDocumentGroupRouter.DELETE("/batch", adminDocument.BatchDeleteDocumentHandler(serverCtx)) + + // Get document detail + adminDocumentGroupRouter.GET("/detail", adminDocument.GetDocumentDetailHandler(serverCtx)) + + // Get document list + adminDocumentGroupRouter.GET("/list", adminDocument.GetDocumentListHandler(serverCtx)) + } + + adminLogGroupRouter := router.Group("/v1/admin/log") + adminLogGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Get message log list + adminLogGroupRouter.GET("/message/list", adminLog.GetMessageLogListHandler(serverCtx)) + } + + adminOrderGroupRouter := router.Group("/v1/admin/order") + adminOrderGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Create order + adminOrderGroupRouter.POST("/", adminOrder.CreateOrderHandler(serverCtx)) + + // Get order list + adminOrderGroupRouter.GET("/list", adminOrder.GetOrderListHandler(serverCtx)) + + // Update order status + adminOrderGroupRouter.PUT("/status", adminOrder.UpdateOrderStatusHandler(serverCtx)) + } + + adminPaymentGroupRouter := router.Group("/v1/admin/payment") + adminPaymentGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Create Payment Method + adminPaymentGroupRouter.POST("/", adminPayment.CreatePaymentMethodHandler(serverCtx)) + + // Update Payment Method + adminPaymentGroupRouter.PUT("/", adminPayment.UpdatePaymentMethodHandler(serverCtx)) + + // Delete Payment Method + adminPaymentGroupRouter.DELETE("/", adminPayment.DeletePaymentMethodHandler(serverCtx)) + + // Get Payment Method List + adminPaymentGroupRouter.GET("/list", adminPayment.GetPaymentMethodListHandler(serverCtx)) + + // Get supported payment platform + adminPaymentGroupRouter.GET("/platform", adminPayment.GetPaymentPlatformHandler(serverCtx)) + } + + adminServerGroupRouter := router.Group("/v1/admin/server") + adminServerGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Update node + adminServerGroupRouter.PUT("/", adminServer.UpdateNodeHandler(serverCtx)) + + // Create node + adminServerGroupRouter.POST("/", adminServer.CreateNodeHandler(serverCtx)) + + // Delete node + adminServerGroupRouter.DELETE("/", adminServer.DeleteNodeHandler(serverCtx)) + + // Batch delete node + adminServerGroupRouter.DELETE("/batch", adminServer.BatchDeleteNodeHandler(serverCtx)) + + // Get node detail + adminServerGroupRouter.GET("/detail", adminServer.GetNodeDetailHandler(serverCtx)) + + // Create node group + adminServerGroupRouter.POST("/group", adminServer.CreateNodeGroupHandler(serverCtx)) + + // Update node group + adminServerGroupRouter.PUT("/group", adminServer.UpdateNodeGroupHandler(serverCtx)) + + // Delete node group + adminServerGroupRouter.DELETE("/group", adminServer.DeleteNodeGroupHandler(serverCtx)) + + // Batch delete node group + adminServerGroupRouter.DELETE("/group/batch", adminServer.BatchDeleteNodeGroupHandler(serverCtx)) + + // Get node group list + adminServerGroupRouter.GET("/group/list", adminServer.GetNodeGroupListHandler(serverCtx)) + + // Get node list + adminServerGroupRouter.GET("/list", adminServer.GetNodeListHandler(serverCtx)) + + // Create rule group + adminServerGroupRouter.POST("/rule_group", adminServer.CreateRuleGroupHandler(serverCtx)) + + // Update rule group + adminServerGroupRouter.PUT("/rule_group", adminServer.UpdateRuleGroupHandler(serverCtx)) + + // Delete rule group + adminServerGroupRouter.DELETE("/rule_group", adminServer.DeleteRuleGroupHandler(serverCtx)) + + // Get rule group list + adminServerGroupRouter.GET("/rule_group_list", adminServer.GetRuleGroupListHandler(serverCtx)) + + // Node sort + adminServerGroupRouter.POST("/sort", adminServer.NodeSortHandler(serverCtx)) + + // Get node tag list + adminServerGroupRouter.GET("/tag/list", adminServer.GetNodeTagListHandler(serverCtx)) + } + + adminSubscribeGroupRouter := router.Group("/v1/admin/subscribe") + adminSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Create subscribe + adminSubscribeGroupRouter.POST("/", adminSubscribe.CreateSubscribeHandler(serverCtx)) + + // Update subscribe + adminSubscribeGroupRouter.PUT("/", adminSubscribe.UpdateSubscribeHandler(serverCtx)) + + // Delete subscribe + adminSubscribeGroupRouter.DELETE("/", adminSubscribe.DeleteSubscribeHandler(serverCtx)) + + // Batch delete subscribe + adminSubscribeGroupRouter.DELETE("/batch", adminSubscribe.BatchDeleteSubscribeHandler(serverCtx)) + + // Get subscribe details + adminSubscribeGroupRouter.GET("/details", adminSubscribe.GetSubscribeDetailsHandler(serverCtx)) + + // Create subscribe group + adminSubscribeGroupRouter.POST("/group", adminSubscribe.CreateSubscribeGroupHandler(serverCtx)) + + // Update subscribe group + adminSubscribeGroupRouter.PUT("/group", adminSubscribe.UpdateSubscribeGroupHandler(serverCtx)) + + // Delete subscribe group + adminSubscribeGroupRouter.DELETE("/group", adminSubscribe.DeleteSubscribeGroupHandler(serverCtx)) + + // Batch delete subscribe group + adminSubscribeGroupRouter.DELETE("/group/batch", adminSubscribe.BatchDeleteSubscribeGroupHandler(serverCtx)) + + // Get subscribe group list + adminSubscribeGroupRouter.GET("/group/list", adminSubscribe.GetSubscribeGroupListHandler(serverCtx)) + + // Get subscribe list + adminSubscribeGroupRouter.GET("/list", adminSubscribe.GetSubscribeListHandler(serverCtx)) + + // Subscribe sort + adminSubscribeGroupRouter.POST("/sort", adminSubscribe.SubscribeSortHandler(serverCtx)) + } + + adminSystemGroupRouter := router.Group("/v1/admin/system") + adminSystemGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Get application + adminSystemGroupRouter.GET("/application", adminSystem.GetApplicationHandler(serverCtx)) + + // Update application + adminSystemGroupRouter.PUT("/application", adminSystem.UpdateApplicationHandler(serverCtx)) + + // Create application + adminSystemGroupRouter.POST("/application", adminSystem.CreateApplicationHandler(serverCtx)) + + // Delete application + adminSystemGroupRouter.DELETE("/application", adminSystem.DeleteApplicationHandler(serverCtx)) + + // update application config + adminSystemGroupRouter.PUT("/application_config", adminSystem.UpdateApplicationConfigHandler(serverCtx)) + + // get application config + adminSystemGroupRouter.GET("/application_config", adminSystem.GetApplicationConfigHandler(serverCtx)) + + // Update application version + adminSystemGroupRouter.PUT("/application_version", adminSystem.UpdateApplicationVersionHandler(serverCtx)) + + // Create application version + adminSystemGroupRouter.POST("/application_version", adminSystem.CreateApplicationVersionHandler(serverCtx)) + + // Delete application + adminSystemGroupRouter.DELETE("/application_version", adminSystem.DeleteApplicationVersionHandler(serverCtx)) + + // Get Currency Config + adminSystemGroupRouter.GET("/currency_config", adminSystem.GetCurrencyConfigHandler(serverCtx)) + + // Update Currency Config + adminSystemGroupRouter.PUT("/currency_config", adminSystem.UpdateCurrencyConfigHandler(serverCtx)) + + // Get Node Multiplier + adminSystemGroupRouter.GET("/get_node_multiplier", adminSystem.GetNodeMultiplierHandler(serverCtx)) + + // Get invite config + adminSystemGroupRouter.GET("/invite_config", adminSystem.GetInviteConfigHandler(serverCtx)) + + // Update invite config + adminSystemGroupRouter.PUT("/invite_config", adminSystem.UpdateInviteConfigHandler(serverCtx)) + + // Get node config + adminSystemGroupRouter.GET("/node_config", adminSystem.GetNodeConfigHandler(serverCtx)) + + // Update node config + adminSystemGroupRouter.PUT("/node_config", adminSystem.UpdateNodeConfigHandler(serverCtx)) + + // get Privacy Policy Config + adminSystemGroupRouter.GET("/privacy", adminSystem.GetPrivacyPolicyConfigHandler(serverCtx)) + + // Update Privacy Policy Config + adminSystemGroupRouter.PUT("/privacy", adminSystem.UpdatePrivacyPolicyConfigHandler(serverCtx)) + + // Get register config + adminSystemGroupRouter.GET("/register_config", adminSystem.GetRegisterConfigHandler(serverCtx)) + + // Update register config + adminSystemGroupRouter.PUT("/register_config", adminSystem.UpdateRegisterConfigHandler(serverCtx)) + + // Set Node Multiplier + adminSystemGroupRouter.POST("/set_node_multiplier", adminSystem.SetNodeMultiplierHandler(serverCtx)) + + // setting telegram bot + adminSystemGroupRouter.POST("/setting_telegram_bot", adminSystem.SettingTelegramBotHandler(serverCtx)) + + // Get site config + adminSystemGroupRouter.GET("/site_config", adminSystem.GetSiteConfigHandler(serverCtx)) + + // Update site config + adminSystemGroupRouter.PUT("/site_config", adminSystem.UpdateSiteConfigHandler(serverCtx)) + + // Get subscribe config + adminSystemGroupRouter.GET("/subscribe_config", adminSystem.GetSubscribeConfigHandler(serverCtx)) + + // Update subscribe config + adminSystemGroupRouter.PUT("/subscribe_config", adminSystem.UpdateSubscribeConfigHandler(serverCtx)) + + // Get subscribe type + adminSystemGroupRouter.GET("/subscribe_type", adminSystem.GetSubscribeTypeHandler(serverCtx)) + + // Get Team of Service Config + adminSystemGroupRouter.GET("/tos_config", adminSystem.GetTosConfigHandler(serverCtx)) + + // Update Team of Service Config + adminSystemGroupRouter.PUT("/tos_config", adminSystem.UpdateTosConfigHandler(serverCtx)) + + // Get Verify Code Config + adminSystemGroupRouter.GET("/verify_code_config", adminSystem.GetVerifyCodeConfigHandler(serverCtx)) + + // Update Verify Code Config + adminSystemGroupRouter.PUT("/verify_code_config", adminSystem.UpdateVerifyCodeConfigHandler(serverCtx)) + + // Get verify config + adminSystemGroupRouter.GET("/verify_config", adminSystem.GetVerifyConfigHandler(serverCtx)) + + // Update verify config + adminSystemGroupRouter.PUT("/verify_config", adminSystem.UpdateVerifyConfigHandler(serverCtx)) + } + + adminTicketGroupRouter := router.Group("/v1/admin/ticket") + adminTicketGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Update ticket status + adminTicketGroupRouter.PUT("/", adminTicket.UpdateTicketStatusHandler(serverCtx)) + + // Get ticket detail + adminTicketGroupRouter.GET("/detail", adminTicket.GetTicketHandler(serverCtx)) + + // Create ticket follow + adminTicketGroupRouter.POST("/follow", adminTicket.CreateTicketFollowHandler(serverCtx)) + + // Get ticket list + adminTicketGroupRouter.GET("/list", adminTicket.GetTicketListHandler(serverCtx)) + } + + adminToolGroupRouter := router.Group("/v1/admin/tool") + adminToolGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Get System Log + adminToolGroupRouter.GET("/log", adminTool.GetSystemLogHandler(serverCtx)) + + // Restart System + adminToolGroupRouter.GET("/restart", adminTool.RestartSystemHandler(serverCtx)) + } + + adminUserGroupRouter := router.Group("/v1/admin/user") + adminUserGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Delete user + adminUserGroupRouter.DELETE("/", adminUser.DeleteUserHandler(serverCtx)) + + // Create user + adminUserGroupRouter.POST("/", adminUser.CreateUserHandler(serverCtx)) + + // Create user auth method + adminUserGroupRouter.POST("/auth_method", adminUser.CreateUserAuthMethodHandler(serverCtx)) + + // Delete user auth method + adminUserGroupRouter.DELETE("/auth_method", adminUser.DeleteUserAuthMethodHandler(serverCtx)) + + // Update user auth method + adminUserGroupRouter.PUT("/auth_method", adminUser.UpdateUserAuthMethodHandler(serverCtx)) + + // Get user auth method + adminUserGroupRouter.GET("/auth_method", adminUser.GetUserAuthMethodHandler(serverCtx)) + + // Update user basic info + adminUserGroupRouter.PUT("/basic", adminUser.UpdateUserBasicInfoHandler(serverCtx)) + + // Batch delete user + adminUserGroupRouter.DELETE("/batch", adminUser.BatchDeleteUserHandler(serverCtx)) + + // Current user + adminUserGroupRouter.GET("/current", adminUser.CurrentUserHandler(serverCtx)) + + // Get user detail + adminUserGroupRouter.GET("/detail", adminUser.GetUserDetailHandler(serverCtx)) + + // User device + adminUserGroupRouter.PUT("/device", adminUser.UpdateUserDeviceHandler(serverCtx)) + + // Delete user device + adminUserGroupRouter.DELETE("/device", adminUser.DeleteUserDeviceHandler(serverCtx)) + + // kick offline user device + adminUserGroupRouter.PUT("/device/kick_offline", adminUser.KickOfflineByUserDeviceHandler(serverCtx)) + + // Get user list + adminUserGroupRouter.GET("/list", adminUser.GetUserListHandler(serverCtx)) + + // Get user login logs + adminUserGroupRouter.GET("/login/logs", adminUser.GetUserLoginLogsHandler(serverCtx)) + + // Update user notify setting + adminUserGroupRouter.PUT("/notify", adminUser.UpdateUserNotifySettingHandler(serverCtx)) + + // Get user subcribe + adminUserGroupRouter.GET("/subscribe", adminUser.GetUserSubscribeHandler(serverCtx)) + + // Create user subcribe + adminUserGroupRouter.POST("/subscribe", adminUser.CreateUserSubscribeHandler(serverCtx)) + + // Update user subcribe + adminUserGroupRouter.PUT("/subscribe", adminUser.UpdateUserSubscribeHandler(serverCtx)) + + // Delete user subcribe + adminUserGroupRouter.DELETE("/subscribe", adminUser.DeleteUserSubscribeHandler(serverCtx)) + + // Get user subcribe by id + adminUserGroupRouter.GET("/subscribe/detail", adminUser.GetUserSubscribeByIdHandler(serverCtx)) + + // Get user subcribe devices + adminUserGroupRouter.GET("/subscribe/device", adminUser.GetUserSubscribeDevicesHandler(serverCtx)) + + // Get user subcribe logs + adminUserGroupRouter.GET("/subscribe/logs", adminUser.GetUserSubscribeLogsHandler(serverCtx)) + + // Get user subcribe traffic logs + adminUserGroupRouter.GET("/subscribe/traffic_logs", adminUser.GetUserSubscribeTrafficLogsHandler(serverCtx)) + } + + appAnnouncementGroupRouter := router.Group("/v1/app/announcement") + appAnnouncementGroupRouter.Use(middleware.AppMiddleware(serverCtx), middleware.AuthMiddleware(serverCtx)) + + { + // Query announcement + appAnnouncementGroupRouter.GET("/list", appAnnouncement.QueryAnnouncementHandler(serverCtx)) + } + + appAuthGroupRouter := router.Group("/v1/app/auth") + appAuthGroupRouter.Use(middleware.AppMiddleware(serverCtx)) + + { + // Check Account + appAuthGroupRouter.POST("/check", appAuth.CheckHandler(serverCtx)) + + // GetAppConfig + appAuthGroupRouter.POST("/config", appAuth.GetAppConfigHandler(serverCtx)) + + // Login + appAuthGroupRouter.POST("/login", appAuth.LoginHandler(serverCtx)) + + // Register + appAuthGroupRouter.POST("/register", appAuth.RegisterHandler(serverCtx)) + + // Reset Password + appAuthGroupRouter.POST("/reset_password", appAuth.ResetPasswordHandler(serverCtx)) + } + + appDocumentGroupRouter := router.Group("/v1/app/document") + appDocumentGroupRouter.Use(middleware.AppMiddleware(serverCtx), middleware.AuthMiddleware(serverCtx)) + + { + // Get document detail + appDocumentGroupRouter.GET("/detail", appDocument.QueryDocumentDetailHandler(serverCtx)) + + // Get document list + appDocumentGroupRouter.GET("/list", appDocument.QueryDocumentListHandler(serverCtx)) + } + + appNodeGroupRouter := router.Group("/v1/app/node") + appNodeGroupRouter.Use(middleware.AppMiddleware(serverCtx), middleware.AuthMiddleware(serverCtx)) + + { + // Get Node list + appNodeGroupRouter.GET("/list", appNode.GetNodeListHandler(serverCtx)) + + // Get rule group list + appNodeGroupRouter.GET("/rule_group_list", appNode.GetRuleGroupListHandler(serverCtx)) + } + + appOrderGroupRouter := router.Group("/v1/app/order") + appOrderGroupRouter.Use(middleware.AppMiddleware(serverCtx), middleware.AuthMiddleware(serverCtx)) + + { + // Checkout order + appOrderGroupRouter.POST("/checkout", appOrder.CheckoutOrderHandler(serverCtx)) + + // Close order + appOrderGroupRouter.POST("/close", appOrder.CloseOrderHandler(serverCtx)) + + // Get order + appOrderGroupRouter.GET("/detail", appOrder.QueryOrderDetailHandler(serverCtx)) + + // Get order list + appOrderGroupRouter.GET("/list", appOrder.QueryOrderListHandler(serverCtx)) + + // Pre create order + appOrderGroupRouter.POST("/pre", appOrder.PreCreateOrderHandler(serverCtx)) + + // purchase Subscription + appOrderGroupRouter.POST("/purchase", appOrder.PurchaseHandler(serverCtx)) + + // Recharge + appOrderGroupRouter.POST("/recharge", appOrder.RechargeHandler(serverCtx)) + + // Renewal Subscription + appOrderGroupRouter.POST("/renewal", appOrder.RenewalHandler(serverCtx)) + + // Reset traffic + appOrderGroupRouter.POST("/reset", appOrder.ResetTrafficHandler(serverCtx)) + } + + appPaymentGroupRouter := router.Group("/v1/app/payment") + appPaymentGroupRouter.Use(middleware.AppMiddleware(serverCtx), middleware.AuthMiddleware(serverCtx)) + + { + // Get available payment methods + appPaymentGroupRouter.GET("/methods", appPayment.GetAvailablePaymentMethodsHandler(serverCtx)) + } + + appSubscribeGroupRouter := router.Group("/v1/app/subscribe") + appSubscribeGroupRouter.Use(middleware.AppMiddleware(serverCtx), middleware.AuthMiddleware(serverCtx)) + + { + // Get application config + appSubscribeGroupRouter.GET("/application/config", appSubscribe.QueryApplicationConfigHandler(serverCtx)) + + // Get subscribe group list + appSubscribeGroupRouter.GET("/group/list", appSubscribe.QuerySubscribeGroupListHandler(serverCtx)) + + // Get subscribe list + appSubscribeGroupRouter.GET("/list", appSubscribe.QuerySubscribeListHandler(serverCtx)) + + // Reset user subscription period + appSubscribeGroupRouter.POST("/reset/period", appSubscribe.ResetUserSubscribePeriodHandler(serverCtx)) + + // Get Already subscribed to package + appSubscribeGroupRouter.GET("/user/already_subscribe", appSubscribe.QueryUserAlreadySubscribeHandler(serverCtx)) + + // Get Available subscriptions for users + appSubscribeGroupRouter.GET("/user/available_subscribe", appSubscribe.QueryUserAvailableUserSubscribeHandler(serverCtx)) + } + + appUserGroupRouter := router.Group("/v1/app/user") + appUserGroupRouter.Use(middleware.AppMiddleware(serverCtx), middleware.AuthMiddleware(serverCtx)) + + { + // Delete Account + appUserGroupRouter.DELETE("/account", appUser.DeleteAccountHandler(serverCtx)) + + // Query User Affiliate Count + appUserGroupRouter.GET("/affiliate/count", appUser.QueryUserAffiliateHandler(serverCtx)) + + // Query User Affiliate List + appUserGroupRouter.GET("/affiliate/list", appUser.QueryUserAffiliateListHandler(serverCtx)) + + // query user info + appUserGroupRouter.GET("/info", appUser.QueryUserInfoHandler(serverCtx)) + + // Get user online time total + appUserGroupRouter.GET("/online_time/statistics", appUser.GetUserOnlineTimeStatisticsHandler(serverCtx)) + + // Update Password + appUserGroupRouter.PUT("/password", appUser.UpdatePasswordHandler(serverCtx)) + + // Get user subcribe traffic logs + appUserGroupRouter.GET("/subscribe/traffic_logs", appUser.GetUserSubscribeTrafficLogsHandler(serverCtx)) + } + + appWsGroupRouter := router.Group("/v1/app/ws") + appWsGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // App heartbeat + appWsGroupRouter.GET("/:userid/:identifier", appWs.AppWsHandler(serverCtx)) + } + + authGroupRouter := router.Group("/v1/auth") + + { + // Check user is exist + authGroupRouter.GET("/check", auth.CheckUserHandler(serverCtx)) + + // Check user telephone is exist + authGroupRouter.GET("/check/telephone", auth.CheckUserTelephoneHandler(serverCtx)) + + // User login + authGroupRouter.POST("/login", auth.UserLoginHandler(serverCtx)) + + // User Telephone login + authGroupRouter.POST("/login/telephone", auth.TelephoneLoginHandler(serverCtx)) + + // User register + authGroupRouter.POST("/register", auth.UserRegisterHandler(serverCtx)) + + // User Telephone register + authGroupRouter.POST("/register/telephone", auth.TelephoneUserRegisterHandler(serverCtx)) + + // Reset password + authGroupRouter.POST("/reset", auth.ResetPasswordHandler(serverCtx)) + + // Reset password + authGroupRouter.POST("/reset/telephone", auth.TelephoneResetPasswordHandler(serverCtx)) + } + + authOauthGroupRouter := router.Group("/v1/auth/oauth") + + { + // Apple Login Callback + authOauthGroupRouter.POST("/callback/apple", authOauth.AppleLoginCallbackHandler(serverCtx)) + + // OAuth login + authOauthGroupRouter.POST("/login", authOauth.OAuthLoginHandler(serverCtx)) + + // OAuth login get token + authOauthGroupRouter.POST("/login/token", authOauth.OAuthLoginGetTokenHandler(serverCtx)) + } + + commonGroupRouter := router.Group("/v1/common") + + { + // Get Ads + commonGroupRouter.GET("/ads", common.GetAdsHandler(serverCtx)) + + // Get Tos Content + commonGroupRouter.GET("/application", common.GetApplicationHandler(serverCtx)) + + // Check verification code + commonGroupRouter.POST("/check_verification_code", common.CheckVerificationCodeHandler(serverCtx)) + + // Get verification code + commonGroupRouter.POST("/send_code", common.SendEmailCodeHandler(serverCtx)) + + // Get sms verification code + commonGroupRouter.POST("/send_sms_code", common.SendSmsCodeHandler(serverCtx)) + + // Get global config + commonGroupRouter.GET("/site/config", common.GetGlobalConfigHandler(serverCtx)) + + // Get Privacy Policy + commonGroupRouter.GET("/site/privacy", common.GetPrivacyPolicyHandler(serverCtx)) + + // Get stat + commonGroupRouter.GET("/site/stat", common.GetStatHandler(serverCtx)) + + // Get Tos Content + commonGroupRouter.GET("/site/tos", common.GetTosHandler(serverCtx)) + } + + publicAnnouncementGroupRouter := router.Group("/v1/public/announcement") + publicAnnouncementGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Query announcement + publicAnnouncementGroupRouter.GET("/list", publicAnnouncement.QueryAnnouncementHandler(serverCtx)) + } + + publicDocumentGroupRouter := router.Group("/v1/public/document") + publicDocumentGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Get document detail + publicDocumentGroupRouter.GET("/detail", publicDocument.QueryDocumentDetailHandler(serverCtx)) + + // Get document list + publicDocumentGroupRouter.GET("/list", publicDocument.QueryDocumentListHandler(serverCtx)) + } + + publicOrderGroupRouter := router.Group("/v1/public/order") + publicOrderGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Close order + publicOrderGroupRouter.POST("/close", publicOrder.CloseOrderHandler(serverCtx)) + + // Get order + publicOrderGroupRouter.GET("/detail", publicOrder.QueryOrderDetailHandler(serverCtx)) + + // Get order list + publicOrderGroupRouter.GET("/list", publicOrder.QueryOrderListHandler(serverCtx)) + + // Pre create order + publicOrderGroupRouter.POST("/pre", publicOrder.PreCreateOrderHandler(serverCtx)) + + // purchase Subscription + publicOrderGroupRouter.POST("/purchase", publicOrder.PurchaseHandler(serverCtx)) + + // Recharge + publicOrderGroupRouter.POST("/recharge", publicOrder.RechargeHandler(serverCtx)) + + // Renewal Subscription + publicOrderGroupRouter.POST("/renewal", publicOrder.RenewalHandler(serverCtx)) + + // Reset traffic + publicOrderGroupRouter.POST("/reset", publicOrder.ResetTrafficHandler(serverCtx)) + } + + publicPaymentGroupRouter := router.Group("/v1/public/payment") + publicPaymentGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Get available payment methods + publicPaymentGroupRouter.GET("/methods", publicPayment.GetAvailablePaymentMethodsHandler(serverCtx)) + } + + publicPortalGroupRouter := router.Group("/v1/public/portal") + + { + // Purchase Checkout + publicPortalGroupRouter.POST("/order/checkout", publicPortal.PurchaseCheckoutHandler(serverCtx)) + + // Query Purchase Order + publicPortalGroupRouter.GET("/order/status", publicPortal.QueryPurchaseOrderHandler(serverCtx)) + + // Get available payment methods + publicPortalGroupRouter.GET("/payment-method", publicPortal.GetAvailablePaymentMethodsHandler(serverCtx)) + + // Pre Purchase Order + publicPortalGroupRouter.POST("/pre", publicPortal.PrePurchaseOrderHandler(serverCtx)) + + // Purchase subscription + publicPortalGroupRouter.POST("/purchase", publicPortal.PurchaseHandler(serverCtx)) + + // Get Subscription + publicPortalGroupRouter.GET("/subscribe", publicPortal.GetSubscriptionHandler(serverCtx)) + } + + publicSubscribeGroupRouter := router.Group("/v1/public/subscribe") + publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Get application config + publicSubscribeGroupRouter.GET("/application/config", publicSubscribe.QueryApplicationConfigHandler(serverCtx)) + + // Get subscribe group list + publicSubscribeGroupRouter.GET("/group/list", publicSubscribe.QuerySubscribeGroupListHandler(serverCtx)) + + // Get subscribe list + publicSubscribeGroupRouter.GET("/list", publicSubscribe.QuerySubscribeListHandler(serverCtx)) + } + + publicTicketGroupRouter := router.Group("/v1/public/ticket") + publicTicketGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Update ticket status + publicTicketGroupRouter.PUT("/", publicTicket.UpdateUserTicketStatusHandler(serverCtx)) + + // Create ticket + publicTicketGroupRouter.POST("/", publicTicket.CreateUserTicketHandler(serverCtx)) + + // Get ticket detail + publicTicketGroupRouter.GET("/detail", publicTicket.GetUserTicketDetailsHandler(serverCtx)) + + // Create ticket follow + publicTicketGroupRouter.POST("/follow", publicTicket.CreateUserTicketFollowHandler(serverCtx)) + + // Get ticket list + publicTicketGroupRouter.GET("/list", publicTicket.GetUserTicketListHandler(serverCtx)) + } + + publicUserGroupRouter := router.Group("/v1/public/user") + publicUserGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Query User Affiliate Count + publicUserGroupRouter.GET("/affiliate/count", publicUser.QueryUserAffiliateHandler(serverCtx)) + + // Query User Affiliate List + publicUserGroupRouter.GET("/affiliate/list", publicUser.QueryUserAffiliateListHandler(serverCtx)) + + // Query User Balance Log + publicUserGroupRouter.GET("/balance_log", publicUser.QueryUserBalanceLogHandler(serverCtx)) + + // Update Bind Email + publicUserGroupRouter.PUT("/bind_email", publicUser.UpdateBindEmailHandler(serverCtx)) + + // Update Bind Mobile + publicUserGroupRouter.PUT("/bind_mobile", publicUser.UpdateBindMobileHandler(serverCtx)) + + // Bind OAuth + publicUserGroupRouter.POST("/bind_oauth", publicUser.BindOAuthHandler(serverCtx)) + + // Bind OAuth Callback + publicUserGroupRouter.POST("/bind_oauth/callback", publicUser.BindOAuthCallbackHandler(serverCtx)) + + // Bind Telegram + publicUserGroupRouter.GET("/bind_telegram", publicUser.BindTelegramHandler(serverCtx)) + + // Query User Commission Log + publicUserGroupRouter.GET("/commission_log", publicUser.QueryUserCommissionLogHandler(serverCtx)) + + // Query User Info + publicUserGroupRouter.GET("/info", publicUser.QueryUserInfoHandler(serverCtx)) + + // Get Login Log + publicUserGroupRouter.GET("/login_log", publicUser.GetLoginLogHandler(serverCtx)) + + // Update User Notify + publicUserGroupRouter.PUT("/notify", publicUser.UpdateUserNotifyHandler(serverCtx)) + + // Get OAuth Methods + publicUserGroupRouter.GET("/oauth_methods", publicUser.GetOAuthMethodsHandler(serverCtx)) + + // Update User Password + publicUserGroupRouter.PUT("/password", publicUser.UpdateUserPasswordHandler(serverCtx)) + + // Query User Subscribe + publicUserGroupRouter.GET("/subscribe", publicUser.QueryUserSubscribeHandler(serverCtx)) + + // Get Subscribe Log + publicUserGroupRouter.GET("/subscribe_log", publicUser.GetSubscribeLogHandler(serverCtx)) + + // Reset User Subscribe Token + publicUserGroupRouter.PUT("/subscribe_token", publicUser.ResetUserSubscribeTokenHandler(serverCtx)) + + // Unbind OAuth + publicUserGroupRouter.POST("/unbind_oauth", publicUser.UnbindOAuthHandler(serverCtx)) + + // Unbind Telegram + publicUserGroupRouter.POST("/unbind_telegram", publicUser.UnbindTelegramHandler(serverCtx)) + + // Unsubscribe + publicUserGroupRouter.POST("/unsubscribe", publicUser.UnsubscribeHandler(serverCtx)) + + // Pre Unsubscribe + publicUserGroupRouter.POST("/unsubscribe/pre", publicUser.PreUnsubscribeHandler(serverCtx)) + + // Verify Email + publicUserGroupRouter.POST("/verify_email", publicUser.VerifyEmailHandler(serverCtx)) + } + + serverGroupRouter := router.Group("/v1/server") + serverGroupRouter.Use(middleware.ServerMiddleware(serverCtx)) + + { + // Get server config + serverGroupRouter.GET("/config", server.GetServerConfigHandler(serverCtx)) + + // Push online users + serverGroupRouter.POST("/online", server.PushOnlineUsersHandler(serverCtx)) + + // Push user Traffic + serverGroupRouter.POST("/push", server.ServerPushUserTrafficHandler(serverCtx)) + + // Push server status + serverGroupRouter.POST("/status", server.ServerPushStatusHandler(serverCtx)) + + // Get user list + serverGroupRouter.GET("/user", server.GetServerUserListHandler(serverCtx)) + } +} diff --git a/internal/handler/server/getServerConfigHandler.go b/internal/handler/server/getServerConfigHandler.go new file mode 100644 index 0000000..e849633 --- /dev/null +++ b/internal/handler/server/getServerConfigHandler.go @@ -0,0 +1,37 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +// GetServerConfigHandler Get server config +func GetServerConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetServerConfigRequest + _ = c.ShouldBind(&req) + _ = c.ShouldBindQuery(&req.ServerCommon) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewGetServerConfigLogic(c, svcCtx) + resp, err := l.GetServerConfig(&req) + if err != nil { + if errors.Is(err, xerr.StatusNotModified) { + c.String(304, "Not Modified") + return + } + c.String(404, "Not Found") + return + } + c.JSON(200, resp) + } +} diff --git a/internal/handler/server/getServerUserListHandler.go b/internal/handler/server/getServerUserListHandler.go new file mode 100644 index 0000000..cfa143c --- /dev/null +++ b/internal/handler/server/getServerUserListHandler.go @@ -0,0 +1,37 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +// Get user list +func GetServerUserListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetServerUserListRequest + _ = c.ShouldBind(&req) + _ = c.ShouldBindQuery(&req.ServerCommon) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewGetServerUserListLogic(c, svcCtx) + resp, err := l.GetServerUserList(&req) + if err != nil { + if errors.Is(err, xerr.StatusNotModified) { + c.String(304, "Not Modified") + return + } + c.String(404, "Not Found") + return + } + c.JSON(200, resp) + } +} diff --git a/internal/handler/server/pushOnlineUsersHandler.go b/internal/handler/server/pushOnlineUsersHandler.go new file mode 100644 index 0000000..0bf8258 --- /dev/null +++ b/internal/handler/server/pushOnlineUsersHandler.go @@ -0,0 +1,27 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Push online users +func PushOnlineUsersHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.OnlineUsersRequest + _ = c.ShouldBind(&req) + _ = c.ShouldBindQuery(&req.ServerCommon) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewPushOnlineUsersLogic(c.Request.Context(), svcCtx) + err := l.PushOnlineUsers(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/server/serverPushStatusHandler.go b/internal/handler/server/serverPushStatusHandler.go new file mode 100644 index 0000000..b690b21 --- /dev/null +++ b/internal/handler/server/serverPushStatusHandler.go @@ -0,0 +1,27 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// Push server status +func ServerPushStatusHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.ServerPushStatusRequest + _ = c.ShouldBind(&req) + _ = c.ShouldBindQuery(&req.ServerCommon) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewServerPushStatusLogic(c.Request.Context(), svcCtx) + err := l.ServerPushStatus(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/server/serverPushUserTrafficHandler.go b/internal/handler/server/serverPushUserTrafficHandler.go new file mode 100644 index 0000000..156f29e --- /dev/null +++ b/internal/handler/server/serverPushUserTrafficHandler.go @@ -0,0 +1,27 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/result" +) + +// ServerPushUserTrafficHandler Push user Traffic +func ServerPushUserTrafficHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.ServerPushUserTrafficRequest + _ = c.ShouldBind(&req) + _ = c.ShouldBindQuery(&req.ServerCommon) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewServerPushUserTrafficLogic(c.Request.Context(), svcCtx) + err := l.ServerPushUserTraffic(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/subscribe.go b/internal/handler/subscribe.go new file mode 100644 index 0000000..391fe38 --- /dev/null +++ b/internal/handler/subscribe.go @@ -0,0 +1,36 @@ +package handler + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" +) + +func SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.SubscribeRequest + if c.Request.Header.Get("token") != "" { + req.Token = c.Request.Header.Get("token") + } else { + req.Token = c.Query("token") + } + req.UA = c.Request.Header.Get("User-Agent") + req.Flag = c.Query("flag") + l := subscribe.NewSubscribeLogic(c, svcCtx) + resp, err := l.Generate(&req) + if err != nil { + return + } + c.Header("subscription-userinfo", resp.Header) + c.String(200, "%s", string(resp.Config)) + } +} + +func RegisterSubscribeHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { + path := serverCtx.Config.Subscribe.SubscribePath + if path == "" { + path = "/api/subscribe" + } + router.GET(path, SubscribeHandler(serverCtx)) +} diff --git a/internal/handler/telegram.go b/internal/handler/telegram.go new file mode 100644 index 0000000..28fb83c --- /dev/null +++ b/internal/handler/telegram.go @@ -0,0 +1,36 @@ +package handler + +import ( + "github.com/gin-gonic/gin" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/perfect-panel/ppanel-server/internal/logic/telegram" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/ppanel-server/pkg/tool" +) + +func RegisterTelegramHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { + router.POST("/v1/telegram/webhook", TelegramHandler(serverCtx)) +} + +func TelegramHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + // auth secret + secret := c.Query("secret") + if secret != tool.Md5Encode(svcCtx.Config.Telegram.BotToken, false) { + logger.WithContext(c.Request.Context()).Error("[TelegramHandler] Secret is wrong", logger.Field("request secret", secret), logger.Field("config secret", tool.Md5Encode(svcCtx.Config.Telegram.BotToken, false)), logger.Field("token", svcCtx.Config.Telegram.BotToken)) + c.Abort() + result.HttpResult(c, nil, nil) + return + } + var request tgbotapi.Update + if err := c.BindJSON(&request); err != nil { + logger.WithContext(c.Request.Context()).Error("[TelegramHandler] Failed to bind request", logger.Field("error", err.Error())) + c.Abort() + result.HttpResult(c, nil, err) + } + l := telegram.NewTelegramLogic(c, svcCtx) + l.TelegramLogic(&request) + } +} diff --git a/internal/logic/admin/ads/createAdsLogic.go b/internal/logic/admin/ads/createAdsLogic.go new file mode 100644 index 0000000..1ee3435 --- /dev/null +++ b/internal/logic/admin/ads/createAdsLogic.go @@ -0,0 +1,44 @@ +package ads + +import ( + "context" + "time" + + "github.com/perfect-panel/ppanel-server/internal/model/ads" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type CreateAdsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Create Ads +func NewCreateAdsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateAdsLogic { + return &CreateAdsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateAdsLogic) CreateAds(req *types.CreateAdsRequest) error { + if err := l.svcCtx.AdsModel.Insert(l.ctx, &ads.Ads{ + Title: req.Title, + Type: req.Type, + Content: req.Content, + TargetURL: req.TargetURL, + StartTime: time.UnixMilli(req.StartTime), + EndTime: time.UnixMilli(req.EndTime), + Status: req.Status, + }); err != nil { + l.Errorw("insert ads error: %v", logger.Field("error", err.Error()), logger.Field("req", req)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert ads error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/ads/deleteAdsLogic.go b/internal/logic/admin/ads/deleteAdsLogic.go new file mode 100644 index 0000000..a9588c8 --- /dev/null +++ b/internal/logic/admin/ads/deleteAdsLogic.go @@ -0,0 +1,34 @@ +package ads + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type DeleteAdsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Delete Ads +func NewDeleteAdsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteAdsLogic { + return &DeleteAdsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteAdsLogic) DeleteAds(req *types.DeleteAdsRequest) error { + if err := l.svcCtx.AdsModel.Delete(l.ctx, req.Id); err != nil { + l.Errorw("delete ads error", logger.Field("error", err.Error()), logger.Field("id", req.Id)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete ads error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/ads/getAdsDetailLogic.go b/internal/logic/admin/ads/getAdsDetailLogic.go new file mode 100644 index 0000000..2897ad3 --- /dev/null +++ b/internal/logic/admin/ads/getAdsDetailLogic.go @@ -0,0 +1,38 @@ +package ads + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetAdsDetailLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Ads Detail +func NewGetAdsDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAdsDetailLogic { + return &GetAdsDetailLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAdsDetailLogic) GetAdsDetail(req *types.GetAdsDetailRequest) (resp *types.Ads, err error) { + data, err := l.svcCtx.AdsModel.FindOne(l.ctx, req.Id) + if err != nil { + l.Errorw("find ads error", logger.Field("error", err.Error()), logger.Field("id", req.Id)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find ads error: %v", err.Error()) + } + resp = new(types.Ads) + tool.DeepCopy(resp, data) + return +} diff --git a/internal/logic/admin/ads/getAdsListLogic.go b/internal/logic/admin/ads/getAdsListLogic.go new file mode 100644 index 0000000..5c531fe --- /dev/null +++ b/internal/logic/admin/ads/getAdsListLogic.go @@ -0,0 +1,45 @@ +package ads + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/ads" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetAdsListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Ads List +func NewGetAdsListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAdsListLogic { + return &GetAdsListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAdsListLogic) GetAdsList(req *types.GetAdsListRequest) (resp *types.GetAdsListResponse, err error) { + total, data, err := l.svcCtx.AdsModel.GetAdsListByPage(l.ctx, req.Page, req.Size, ads.Filter{ + Search: req.Search, + Status: req.Status, + }) + if err != nil { + l.Errorw("get ads list error", logger.Field("error", err.Error()), logger.Field("req", req)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get ads list error: %v", err.Error()) + } + resp = &types.GetAdsListResponse{ + Total: total, + List: make([]types.Ads, len(data)), + } + tool.DeepCopy(&resp.List, data) + return +} diff --git a/internal/logic/admin/ads/updateAdsLogic.go b/internal/logic/admin/ads/updateAdsLogic.go new file mode 100644 index 0000000..bf99931 --- /dev/null +++ b/internal/logic/admin/ads/updateAdsLogic.go @@ -0,0 +1,44 @@ +package ads + +import ( + "context" + "time" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateAdsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Update Ads +func NewUpdateAdsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateAdsLogic { + return &UpdateAdsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateAdsLogic) UpdateAds(req *types.UpdateAdsRequest) error { + data, err := l.svcCtx.AdsModel.FindOne(l.ctx, req.Id) + if err != nil { + l.Errorw("find ads error", logger.Field("error", err.Error()), logger.Field("id", req.Id)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find ads error: %v", err.Error()) + } + tool.DeepCopy(data, req) + data.StartTime = time.UnixMilli(req.StartTime) + data.EndTime = time.UnixMilli(req.EndTime) + if err := l.svcCtx.AdsModel.Update(l.ctx, data); err != nil { + l.Errorw("update ads error", logger.Field("error", err.Error()), logger.Field("req", req)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update ads error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/announcement/createAnnouncementLogic.go b/internal/logic/admin/announcement/createAnnouncementLogic.go new file mode 100644 index 0000000..d0ada9f --- /dev/null +++ b/internal/logic/admin/announcement/createAnnouncementLogic.go @@ -0,0 +1,40 @@ +package announcement + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/announcement" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type CreateAnnouncementLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Create announcement +func NewCreateAnnouncementLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateAnnouncementLogic { + return &CreateAnnouncementLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateAnnouncementLogic) CreateAnnouncement(req *types.CreateAnnouncementRequest) error { + + if err := l.svcCtx.AnnouncementModel.Insert(l.ctx, &announcement.Announcement{ + Title: req.Title, + Content: req.Content, + }); err != nil { + l.Errorw("[CreateAnnouncement] Database Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create announcement failed: %v", err.Error()) + } + + return nil +} diff --git a/internal/logic/admin/announcement/deleteAnnouncementLogic.go b/internal/logic/admin/announcement/deleteAnnouncementLogic.go new file mode 100644 index 0000000..edee3ac --- /dev/null +++ b/internal/logic/admin/announcement/deleteAnnouncementLogic.go @@ -0,0 +1,34 @@ +package announcement + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type DeleteAnnouncementLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Delete announcement +func NewDeleteAnnouncementLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteAnnouncementLogic { + return &DeleteAnnouncementLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteAnnouncementLogic) DeleteAnnouncement(req *types.DeleteAnnouncementRequest) error { + if err := l.svcCtx.AnnouncementModel.Delete(l.ctx, req.Id); err != nil { + l.Errorw("[DeleteAnnouncement] Database Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete announcement failed: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/announcement/getAnnouncementListLogic.go b/internal/logic/admin/announcement/getAnnouncementListLogic.go new file mode 100644 index 0000000..1b15b87 --- /dev/null +++ b/internal/logic/admin/announcement/getAnnouncementListLogic.go @@ -0,0 +1,46 @@ +package announcement + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/announcement" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type GetAnnouncementListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get announcement list +func NewGetAnnouncementListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAnnouncementListLogic { + return &GetAnnouncementListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAnnouncementListLogic) GetAnnouncementList(req *types.GetAnnouncementListRequest) (resp *types.GetAnnouncementListResponse, err error) { + total, list, err := l.svcCtx.AnnouncementModel.GetAnnouncementListByPage(l.ctx, int(req.Page), int(req.Size), announcement.Filter{ + Show: req.Show, + Pinned: req.Pinned, + Popup: req.Popup, + Search: req.Search, + }) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetAnnouncementListByPage error: %v", err.Error()) + } + resp = &types.GetAnnouncementListResponse{} + resp.Total = total + resp.List = make([]types.Announcement, 0) + tool.DeepCopy(&resp.List, list) + return +} diff --git a/internal/logic/admin/announcement/getAnnouncementLogic.go b/internal/logic/admin/announcement/getAnnouncementLogic.go new file mode 100644 index 0000000..9139d03 --- /dev/null +++ b/internal/logic/admin/announcement/getAnnouncementLogic.go @@ -0,0 +1,38 @@ +package announcement + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetAnnouncementLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get announcement +func NewGetAnnouncementLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAnnouncementLogic { + return &GetAnnouncementLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAnnouncementLogic) GetAnnouncement(req *types.GetAnnouncementRequest) (resp *types.Announcement, err error) { + info, err := l.svcCtx.AnnouncementModel.FindOne(l.ctx, req.Id) + if err != nil { + l.Errorw("[GetAnnouncement] Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get announcement error: %v", err.Error()) + } + resp = &types.Announcement{} + tool.DeepCopy(resp, info) + return +} diff --git a/internal/logic/admin/announcement/updateAnnouncementLogic.go b/internal/logic/admin/announcement/updateAnnouncementLogic.go new file mode 100644 index 0000000..50d217b --- /dev/null +++ b/internal/logic/admin/announcement/updateAnnouncementLogic.go @@ -0,0 +1,51 @@ +package announcement + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateAnnouncementLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Update announcement +func NewUpdateAnnouncementLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateAnnouncementLogic { + return &UpdateAnnouncementLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateAnnouncementLogic) UpdateAnnouncement(req *types.UpdateAnnouncementRequest) error { + info, err := l.svcCtx.AnnouncementModel.FindOne(l.ctx, req.Id) + if err != nil { + l.Errorw("[UpdateAnnouncement] Query Database Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get announcement error: %v", err.Error()) + } + info.Title = req.Title + info.Content = req.Content + if req.Show != nil { + info.Show = req.Show + } + if req.Pinned != nil { + info.Pinned = req.Pinned + } + if req.Popup != nil { + info.Popup = req.Popup + } + err = l.svcCtx.AnnouncementModel.Update(l.ctx, info) + if err != nil { + l.Errorw("[UpdateAnnouncement] Update Database Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update announcement error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/authMethod/getAuthMethodConfigLogic.go b/internal/logic/admin/authMethod/getAuthMethodConfigLogic.go new file mode 100644 index 0000000..c0ac121 --- /dev/null +++ b/internal/logic/admin/authMethod/getAuthMethodConfigLogic.go @@ -0,0 +1,46 @@ +package authMethod + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetAuthMethodConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get auth method config +func NewGetAuthMethodConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAuthMethodConfigLogic { + return &GetAuthMethodConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAuthMethodConfigLogic) GetAuthMethodConfig(req *types.GetAuthMethodConfigRequest) (resp *types.AuthMethodConfig, err error) { + method, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, req.Method) + if err != nil { + l.Errorw("find one by method failed", logger.Field("method", req.Method), logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find one by method failed: %v", err.Error()) + } + + resp = new(types.AuthMethodConfig) + tool.DeepCopy(resp, method) + if method.Config != "" { + if err := json.Unmarshal([]byte(method.Config), &resp.Config); err != nil { + l.Errorw("unmarshal config failed", logger.Field("config", method.Config), logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "unmarshal apple config failed: %v", err.Error()) + } + } + return +} diff --git a/internal/logic/admin/authMethod/getAuthMethodListLogic.go b/internal/logic/admin/authMethod/getAuthMethodListLogic.go new file mode 100644 index 0000000..5265064 --- /dev/null +++ b/internal/logic/admin/authMethod/getAuthMethodListLogic.go @@ -0,0 +1,49 @@ +package authMethod + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetAuthMethodListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewGetAuthMethodListLogic Get auth method list +func NewGetAuthMethodListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAuthMethodListLogic { + return &GetAuthMethodListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAuthMethodListLogic) GetAuthMethodList() (resp *types.GetAuthMethodListResponse, err error) { + methods, err := l.svcCtx.AuthModel.FindAll(l.ctx) + if err != nil { + l.Errorw("find all failed", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find all failed: %v", err.Error()) + } + var list []types.AuthMethodConfig + for _, method := range methods { + var item types.AuthMethodConfig + tool.DeepCopy(&item, method) + if method.Config != "" { + if err := json.Unmarshal([]byte(method.Config), &item.Config); err != nil { + l.Errorw("unmarshal config failed", logger.Field("config", method.Config), logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "unmarshal config failed: %v", err.Error()) + } + } + list = append(list, item) + } + return &types.GetAuthMethodListResponse{List: list}, nil +} diff --git a/internal/logic/admin/authMethod/getEmailPlatformLogic.go b/internal/logic/admin/authMethod/getEmailPlatformLogic.go new file mode 100644 index 0000000..6e6a607 --- /dev/null +++ b/internal/logic/admin/authMethod/getEmailPlatformLogic.go @@ -0,0 +1,32 @@ +package authMethod + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/email" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type GetEmailPlatformLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get email support platform +func NewGetEmailPlatformLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetEmailPlatformLogic { + return &GetEmailPlatformLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetEmailPlatformLogic) GetEmailPlatform() (resp *types.PlatformResponse, err error) { + return &types.PlatformResponse{ + List: email.GetSupportedPlatforms(), + }, nil +} diff --git a/internal/logic/admin/authMethod/getSmsPlatformLogic.go b/internal/logic/admin/authMethod/getSmsPlatformLogic.go new file mode 100644 index 0000000..a612a50 --- /dev/null +++ b/internal/logic/admin/authMethod/getSmsPlatformLogic.go @@ -0,0 +1,32 @@ +package authMethod + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/sms" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type GetSmsPlatformLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get sms support platform +func NewGetSmsPlatformLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSmsPlatformLogic { + return &GetSmsPlatformLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetSmsPlatformLogic) GetSmsPlatform() (resp *types.PlatformResponse, err error) { + return &types.PlatformResponse{ + List: sms.GetSupportedPlatforms(), + }, nil +} diff --git a/internal/logic/admin/authMethod/testEmailSendLogic.go b/internal/logic/admin/authMethod/testEmailSendLogic.go new file mode 100644 index 0000000..8c071ba --- /dev/null +++ b/internal/logic/admin/authMethod/testEmailSendLogic.go @@ -0,0 +1,41 @@ +package authMethod + +import ( + "context" + "fmt" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/email" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type TestEmailSendLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Test email send +func NewTestEmailSendLogic(ctx context.Context, svcCtx *svc.ServiceContext) *TestEmailSendLogic { + return &TestEmailSendLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *TestEmailSendLogic) TestEmailSend(req *types.TestEmailSendRequest) error { + client, err := email.NewSender(l.svcCtx.Config.Email.Platform, l.svcCtx.Config.Email.PlatformConfig, l.svcCtx.Config.Site.SiteName) + if err != nil { + l.Errorw("new email sender err", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "new email sender err: %v", err.Error()) + } + err = client.Send([]string{req.Email}, "Test Email Send", "this a test email send by ppanel") + if err != nil { + return errors.Wrapf(xerr.NewErrCodeMsg(500, fmt.Sprintf("send email err: %v", err.Error())), "send email err: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/authMethod/testSmsSendLogic.go b/internal/logic/admin/authMethod/testSmsSendLogic.go new file mode 100644 index 0000000..75e9f4d --- /dev/null +++ b/internal/logic/admin/authMethod/testSmsSendLogic.go @@ -0,0 +1,42 @@ +package authMethod + +import ( + "context" + "fmt" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/sms" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type TestSmsSendLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Test sms send +func NewTestSmsSendLogic(ctx context.Context, svcCtx *svc.ServiceContext) *TestSmsSendLogic { + return &TestSmsSendLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *TestSmsSendLogic) TestSmsSend(req *types.TestSmsSendRequest) error { + client, err := sms.NewSender(l.svcCtx.Config.Mobile.Platform, l.svcCtx.Config.Mobile.PlatformConfig) + if err != nil { + l.Errorw("new sms sender err", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "new sms sender err: %v", err.Error()) + } + err = client.SendCode(req.AreaCode, req.Telephone, "123456") + if err != nil { + l.Errorw("send sms err", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCodeMsg(500, fmt.Sprintf("send sms err: %v", err.Error())), "send sms err: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/authMethod/updateAuthMethodConfigLogic.go b/internal/logic/admin/authMethod/updateAuthMethodConfigLogic.go new file mode 100644 index 0000000..57cb46b --- /dev/null +++ b/internal/logic/admin/authMethod/updateAuthMethodConfigLogic.go @@ -0,0 +1,126 @@ +package authMethod + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/ppanel-server/initialize" + "github.com/perfect-panel/ppanel-server/internal/model/auth" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/email" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/sms" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateAuthMethodConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Update auth method config +func NewUpdateAuthMethodConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateAuthMethodConfigLogic { + return &UpdateAuthMethodConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateAuthMethodConfigLogic) UpdateAuthMethodConfig(req *types.UpdateAuthMethodConfigRequest) (resp *types.AuthMethodConfig, err error) { + method, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, req.Method) + if err != nil { + l.Errorw("find one by method failed", logger.Field("method", req.Method), logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find one by method failed: %v", err.Error()) + } + + tool.DeepCopy(method, req) + if req.Config != nil { + if value, ok := req.Config.(map[string]interface{}); ok { + if req.Method == "email" && value["verify_email_template"] == "" { + value["verify_email_template"] = email.DefaultEmailVerifyTemplate + } + if req.Method == "email" && value["expiration_email_template"] == "" { + value["expiration_email_template"] = email.DefaultExpirationEmailTemplate + } + if req.Method == "email" && value["maintenance_email_template"] == "" { + value["maintenance_email_template"] = email.DefaultMaintenanceEmailTemplate + } + if req.Method == "email" && value["traffic_exceed_email_template"] == "" { + value["traffic_exceed_email_template"] = email.DefaultTrafficExceedEmailTemplate + } + + if value["platform_config"] != nil { + platformConfig, err := validatePlatformConfig(value["platform"].(string), value["platform_config"].(map[string]interface{})) + if err != nil { + l.Errorw("validate platform config failed", logger.Field("config", req.Config), logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "validate platform config failed: %v", err.Error()) + } + req.Config.(map[string]interface{})["platform_config"] = platformConfig + } + } + bytes, err := json.Marshal(req.Config) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "marshal config failed: %v", err.Error()) + } + method.Config = string(bytes) + } + err = l.svcCtx.AuthModel.Update(l.ctx, method) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "update auth method failed: %v", err.Error()) + } + + resp = new(types.AuthMethodConfig) + tool.DeepCopy(resp, method) + if method.Config != "" { + if err := json.Unmarshal([]byte(method.Config), &resp.Config); err != nil { + l.Errorw("unmarshal apple config failed", logger.Field("config", method.Config), logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "unmarshal apple config failed: %v", err.Error()) + } + } + // update global config + defer l.UpdateGlobal(method.Method) + return +} + +func (l *UpdateAuthMethodConfigLogic) UpdateGlobal(method string) { + if method == "email" { + initialize.Email(l.svcCtx) + } + if method == "mobile" { + initialize.Mobile(l.svcCtx) + } +} + +func validatePlatformConfig(platform string, cfg map[string]interface{}) (interface{}, error) { + var bytes []byte + var err error + var config interface{} + bytes, err = json.Marshal(cfg) + if err != nil { + return nil, err + } + switch platform { + case email.SMTP.String(): + config = new(auth.SMTPConfig) + case sms.Abosend.String(): + config = new(auth.AbosendConfig) + case sms.AlibabaCloud.String(): + config = new(auth.AlibabaCloudConfig) + case sms.Smsbao.String(): + config = new(auth.SmsbaoConfig) + case sms.Twilio.String(): + config = new(auth.TwilioConfig) + default: + return nil, errors.New("invalid platform") + } + err = json.Unmarshal(bytes, config) + if err != nil { + return nil, err + } + return config, nil +} diff --git a/internal/logic/admin/authMethod/validate_test.go b/internal/logic/admin/authMethod/validate_test.go new file mode 100644 index 0000000..891eda2 --- /dev/null +++ b/internal/logic/admin/authMethod/validate_test.go @@ -0,0 +1,21 @@ +package authMethod + +import ( + "encoding/json" + "testing" + + "github.com/perfect-panel/ppanel-server/pkg/sms" +) + +func TestValidate(t *testing.T) { + config := " {\"0\":\"{\",\"1\":\"\\\"\",\"10\":\"y\",\"11\":\"I\",\"12\":\"d\",\"13\":\"\\\"\",\"14\":\":\",\"15\":\"\\\"\",\"16\":\"\\\"\",\"17\":\",\",\"18\":\"\\\"\",\"19\":\"A\",\"2\":\"A\",\"20\":\"c\",\"21\":\"c\",\"22\":\"e\",\"23\":\"s\",\"24\":\"s\",\"25\":\"K\",\"26\":\"e\",\"27\":\"y\",\"28\":\"S\",\"29\":\"e\",\"3\":\"c\",\"30\":\"c\",\"31\":\"r\",\"32\":\"e\",\"33\":\"t\",\"34\":\"\\\"\",\"35\":\":\",\"36\":\"\\\"\",\"37\":\"\\\"\",\"38\":\",\",\"39\":\"\\\"\",\"4\":\"c\",\"40\":\"S\",\"41\":\"i\",\"42\":\"g\",\"43\":\"n\",\"44\":\"N\",\"45\":\"a\",\"46\":\"m\",\"47\":\"e\",\"48\":\"\\\"\",\"49\":\":\",\"5\":\"e\",\"50\":\"\\\"\",\"51\":\"\\\"\",\"52\":\",\",\"53\":\"\\\"\",\"54\":\"E\",\"55\":\"n\",\"56\":\"d\",\"57\":\"p\",\"58\":\"o\",\"59\":\"i\",\"6\":\"s\",\"60\":\"n\",\"61\":\"t\",\"62\":\"\\\"\",\"63\":\":\",\"64\":\"\\\"\",\"65\":\"\\\"\",\"66\":\",\",\"67\":\"\\\"\",\"68\":\"V\",\"69\":\"e\",\"7\":\"s\",\"70\":\"r\",\"71\":\"i\",\"72\":\"f\",\"73\":\"y\",\"74\":\"T\",\"75\":\"e\",\"76\":\"m\",\"77\":\"p\",\"78\":\"l\",\"79\":\"a\",\"8\":\"K\",\"80\":\"t\",\"81\":\"e\",\"82\":\"C\",\"83\":\"o\",\"84\":\"d\",\"85\":\"e\",\"86\":\"\\\"\",\"87\":\":\",\"88\":\"\\\"\",\"89\":\"\\\"\",\"9\":\"e\",\"90\":\"}\",\"access\":\"xxxx\",\"secret\":\"SSxxxxxxxxxxxxxxxxxxxxxxxU\",\"template\":\"Your verification code is: {{.code}}\"}" + var mapConfig map[string]interface{} + if err := json.Unmarshal([]byte(config), &mapConfig); err != nil { + t.Error(err) + } + platformConfig, err := validatePlatformConfig(sms.Abosend.String(), mapConfig) + if err != nil { + t.Errorf("validateEmailPlatformConfig error: %v", err) + } + t.Logf("platformConfig: %+v", platformConfig) +} diff --git a/internal/logic/admin/console/queryRevenueStatisticsLogic.go b/internal/logic/admin/console/queryRevenueStatisticsLogic.go new file mode 100644 index 0000000..70e5e78 --- /dev/null +++ b/internal/logic/admin/console/queryRevenueStatisticsLogic.go @@ -0,0 +1,77 @@ +package console + +import ( + "context" + "time" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type QueryRevenueStatisticsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Query revenue statistics +func NewQueryRevenueStatisticsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryRevenueStatisticsLogic { + return &QueryRevenueStatisticsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryRevenueStatisticsLogic) QueryRevenueStatistics() (resp *types.RevenueStatisticsResponse, err error) { + + var today, monthly, all types.OrdersStatistics + now := time.Now() + // Get today's revenue statistics + todayData, err := l.svcCtx.OrderModel.QueryDateOrders(l.ctx, now) + if err != nil { + l.Errorw("[QueryRevenueStatisticsLogic] QueryDateOrders error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryDateOrders error: %v", err) + } else { + today = types.OrdersStatistics{ + AmountTotal: todayData.AmountTotal, + NewOrderAmount: todayData.NewOrderAmount, + RenewalOrderAmount: todayData.RenewalOrderAmount, + } + } + // Get monthly's revenue statistics + monthlyData, err := l.svcCtx.OrderModel.QueryMonthlyOrders(l.ctx, now) + if err != nil { + l.Errorw("[QueryRevenueStatisticsLogic] QueryDateOrders error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryDateOrders error: %v", err) + } else { + monthly = types.OrdersStatistics{ + AmountTotal: monthlyData.AmountTotal, + NewOrderAmount: monthlyData.NewOrderAmount, + RenewalOrderAmount: monthlyData.RenewalOrderAmount, + List: make([]types.OrdersStatistics, 0), + } + } + + // Get all revenue statistics + allData, err := l.svcCtx.OrderModel.QueryTotalOrders(l.ctx) + if err != nil { + l.Errorw("[QueryRevenueStatisticsLogic] QueryTotalOrders error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryTotalOrders error: %v", err) + } else { + all = types.OrdersStatistics{ + AmountTotal: allData.AmountTotal, + NewOrderAmount: allData.NewOrderAmount, + RenewalOrderAmount: allData.RenewalOrderAmount, + List: make([]types.OrdersStatistics, 0), + } + } + return &types.RevenueStatisticsResponse{ + Today: today, + Monthly: monthly, + All: all, + }, nil +} diff --git a/internal/logic/admin/console/queryServerTotalDataLogic.go b/internal/logic/admin/console/queryServerTotalDataLogic.go new file mode 100644 index 0000000..6ec85b4 --- /dev/null +++ b/internal/logic/admin/console/queryServerTotalDataLogic.go @@ -0,0 +1,140 @@ +package console + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/pkg/errors" +) + +type QueryServerTotalDataLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewQueryServerTotalDataLogic Query server total data +func NewQueryServerTotalDataLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryServerTotalDataLogic { + return &QueryServerTotalDataLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryServerTotalDataLogic) QueryServerTotalData() (resp *types.ServerTotalDataResponse, err error) { + resp = &types.ServerTotalDataResponse{ + ServerTrafficRankingToday: make([]types.ServerTrafficData, 0), + ServerTrafficRankingYesterday: make([]types.ServerTrafficData, 0), + UserTrafficRankingToday: make([]types.UserTrafficData, 0), + UserTrafficRankingYesterday: make([]types.UserTrafficData, 0), + } + + // Query node server status + servers, err := l.svcCtx.ServerModel.FindAllServer(l.ctx) + if err != nil { + l.Errorw("[QueryServerTotalDataLogic] FindAllServer error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(err, "FindAllServer error: %v", err) + } + onlineServers, err := l.svcCtx.NodeCache.GetOnlineNodeStatusCount(l.ctx) + if err != nil { + l.Errorw("[QueryServerTotalDataLogic] GetOnlineNodeStatusCount error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(err, "GetOnlineNodeStatusCount error: %v", err) + } + resp.OnlineServers = onlineServers + resp.OfflineServers = int64(len(servers) - int(onlineServers)) + + // 获取所有节点在线用户 + allNodeOnlineUser, err := l.svcCtx.NodeCache.GetAllNodeOnlineUser(l.ctx) + if err != nil { + l.Errorw("[QueryServerTotalDataLogic] Get all node online user failed", logger.Field("error", err.Error())) + } + resp.OnlineUserIPs = int64(len(allNodeOnlineUser)) + + // 获取所有节点今日上传下载流量 + allNodeUploadTraffic, err := l.svcCtx.NodeCache.GetAllNodeUploadTraffic(l.ctx) + if err != nil { + l.Errorw("[QueryServerTotalDataLogic] Get all node upload traffic failed", logger.Field("error", err.Error())) + } + resp.TodayUpload = allNodeUploadTraffic + allNodeDownloadTraffic, err := l.svcCtx.NodeCache.GetAllNodeDownloadTraffic(l.ctx) + if err != nil { + l.Errorw("[QueryServerTotalDataLogic] Get all node download traffic failed", logger.Field("error", err.Error())) + } + resp.TodayDownload = allNodeDownloadTraffic + // 获取节点流量排行榜 前10 + nodeTrafficRankingToday, err := l.svcCtx.NodeCache.GetNodeTodayTotalTrafficRank(l.ctx, 10) + if err != nil { + l.Errorw("[QueryServerTotalDataLogic] Get node today total traffic rank failed", logger.Field("error", err.Error())) + } + if len(nodeTrafficRankingToday) > 0 { + var serverTrafficData []types.ServerTrafficData + for _, rank := range nodeTrafficRankingToday { + serverInfo, err := l.svcCtx.ServerModel.FindOne(l.ctx, rank.ID) + if err != nil { + l.Errorw("[QueryServerTotalDataLogic] FindOne error", logger.Field("error", err)) + continue + } + serverTrafficData = append(serverTrafficData, types.ServerTrafficData{ + ServerId: rank.ID, + Name: serverInfo.Name, + Upload: rank.Upload, + Download: rank.Download, + }) + } + resp.ServerTrafficRankingToday = serverTrafficData + } + // 获取用户流量排行榜 前10 + userTrafficRankingToday, err := l.svcCtx.NodeCache.GetUserTodayTotalTrafficRank(l.ctx, 10) + if err != nil { + l.Errorw("[QueryServerTotalDataLogic] Get user today total traffic rank failed", logger.Field("error", err.Error())) + } + + if len(userTrafficRankingToday) > 0 { + var userTrafficData []types.UserTrafficData + for _, rank := range userTrafficRankingToday { + userTrafficData = append(userTrafficData, types.UserTrafficData{ + SID: rank.SID, + Upload: rank.Upload, + Download: rank.Download, + }) + } + resp.UserTrafficRankingToday = userTrafficData + } + // 获取昨日节点流量排行榜 前10 + nodeTrafficRankingYesterday, err := l.svcCtx.NodeCache.GetYesterdayNodeTotalTrafficRank(l.ctx) + if err != nil { + l.Errorw("[QueryServerTotalDataLogic] Get yesterday node total traffic rank failed", logger.Field("error", err.Error())) + } + if len(nodeTrafficRankingYesterday) > 0 { + var serverTrafficData []types.ServerTrafficData + for _, rank := range nodeTrafficRankingYesterday { + serverTrafficData = append(serverTrafficData, types.ServerTrafficData{ + ServerId: rank.ID, + Name: rank.Name, + Upload: rank.Upload, + Download: rank.Download, + }) + } + resp.ServerTrafficRankingYesterday = serverTrafficData + } + // 获取昨日用户流量排行榜 前10 + userTrafficRankingYesterday, err := l.svcCtx.NodeCache.GetYesterdayUserTotalTrafficRank(l.ctx) + if err != nil { + l.Errorw("[QueryServerTotalDataLogic] Get yesterday user total traffic rank failed", logger.Field("error", err.Error())) + } + if len(userTrafficRankingYesterday) > 0 { + var userTrafficData []types.UserTrafficData + for _, rank := range userTrafficRankingYesterday { + userTrafficData = append(userTrafficData, types.UserTrafficData{ + SID: rank.SID, + Upload: rank.Upload, + Download: rank.Download, + }) + } + resp.UserTrafficRankingYesterday = userTrafficData + } + return resp, nil +} diff --git a/internal/logic/admin/console/queryTicketWaitReplyLogic.go b/internal/logic/admin/console/queryTicketWaitReplyLogic.go new file mode 100644 index 0000000..e65984f --- /dev/null +++ b/internal/logic/admin/console/queryTicketWaitReplyLogic.go @@ -0,0 +1,35 @@ +package console + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type QueryTicketWaitReplyLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewQueryTicketWaitReplyLogic Query ticket wait reply +func NewQueryTicketWaitReplyLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryTicketWaitReplyLogic { + return &QueryTicketWaitReplyLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryTicketWaitReplyLogic) QueryTicketWaitReply() (resp *types.TicketWaitRelpyResponse, err error) { + count, err := l.svcCtx.TicketModel.QueryWaitReplyTotal(l.ctx) + if err != nil { + l.Errorw("[QueryTicketWaitReply] Query Database Error: ", logger.Field("error", err.Error())) + return nil, err + } + return &types.TicketWaitRelpyResponse{ + Count: count, + }, nil +} diff --git a/internal/logic/admin/console/queryUserStatisticsLogic.go b/internal/logic/admin/console/queryUserStatisticsLogic.go new file mode 100644 index 0000000..4e1d221 --- /dev/null +++ b/internal/logic/admin/console/queryUserStatisticsLogic.go @@ -0,0 +1,71 @@ +package console + +import ( + "context" + "time" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type QueryUserStatisticsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Query user statistics +func NewQueryUserStatisticsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserStatisticsLogic { + return &QueryUserStatisticsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryUserStatisticsLogic) QueryUserStatistics() (resp *types.UserStatisticsResponse, err error) { + resp = &types.UserStatisticsResponse{} + now := time.Now() + // query today user register count + todayUserResisterCount, err := l.svcCtx.UserModel.QueryResisterUserTotalByDate(l.ctx, now) + if err != nil { + l.Errorw("[QueryUserStatisticsLogic] QueryResisterUserTotalByDate error", logger.Field("error", err.Error())) + } else { + resp.Today.Register = todayUserResisterCount + } + // query today user purchase count + newToday, renewalToday, err := l.svcCtx.OrderModel.QueryDateUserCounts(l.ctx, now) + if err != nil { + l.Errorw("[QueryUserStatisticsLogic] QueryDateUserCounts error", logger.Field("error", err.Error())) + } else { + resp.Today.NewOrderUsers = newToday + resp.Today.RenewalOrderUsers = renewalToday + } + // query month user register count + monthUserResisterCount, err := l.svcCtx.UserModel.QueryResisterUserTotalByMonthly(l.ctx, now) + if err != nil { + l.Errorw("[QueryUserStatisticsLogic] QueryResisterUserTotalByMonthly error", logger.Field("error", err.Error())) + } else { + resp.Monthly.Register = monthUserResisterCount + } + // query month user purchase count + newMonth, renewalMonth, err := l.svcCtx.OrderModel.QueryMonthlyUserCounts(l.ctx, now) + if err != nil { + l.Errorw("[QueryUserStatisticsLogic] QueryMonthlyUserCounts error", logger.Field("error", err.Error())) + } else { + resp.Monthly.NewOrderUsers = newMonth + resp.Monthly.RenewalOrderUsers = renewalMonth + // TODO: Check the purchase status in the past seven days + resp.Monthly.List = make([]types.UserStatistics, 0) + } + + // query all user count + allUserCount, err := l.svcCtx.UserModel.QueryResisterUserTotal(l.ctx) + if err != nil { + l.Errorw("[QueryUserStatisticsLogic] QueryResisterUserTotal error", logger.Field("error", err.Error())) + } else { + resp.All.Register = allUserCount + } + return +} diff --git a/internal/logic/admin/coupon/batchDeleteCouponLogic.go b/internal/logic/admin/coupon/batchDeleteCouponLogic.go new file mode 100644 index 0000000..9e9da2c --- /dev/null +++ b/internal/logic/admin/coupon/batchDeleteCouponLogic.go @@ -0,0 +1,36 @@ +package coupon + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type BatchDeleteCouponLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Batch delete coupon +func NewBatchDeleteCouponLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BatchDeleteCouponLogic { + return &BatchDeleteCouponLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *BatchDeleteCouponLogic) BatchDeleteCoupon(req *types.BatchDeleteCouponRequest) error { + // batch delete coupon by ids + err := l.svcCtx.CouponModel.BatchDelete(l.ctx, req.Ids) + if err != nil { + l.Errorw("[BatchDeleteCoupon] Database Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "batch delete coupon error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/coupon/createCouponLogic.go b/internal/logic/admin/coupon/createCouponLogic.go new file mode 100644 index 0000000..26ff184 --- /dev/null +++ b/internal/logic/admin/coupon/createCouponLogic.go @@ -0,0 +1,49 @@ +package coupon + +import ( + "context" + "math/rand" + "time" + + "github.com/perfect-panel/ppanel-server/internal/model/coupon" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/random" + "github.com/perfect-panel/ppanel-server/pkg/snowflake" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type CreateCouponLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Create coupon +func NewCreateCouponLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateCouponLogic { + return &CreateCouponLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateCouponLogic) CreateCoupon(req *types.CreateCouponRequest) error { + if req.Code == "" { + rand.NewSource(time.Now().UnixNano()) + sid := snowflake.GetID() + req.Code = random.KeyNew(4, 2) + "-" + random.StrToDashedString(random.EncodeBase36(sid)) + } + couponInfo := &coupon.Coupon{} + tool.DeepCopy(couponInfo, req) + couponInfo.Subscribe = tool.Int64SliceToString(req.Subscribe) + err := l.svcCtx.CouponModel.Insert(l.ctx, couponInfo) + if err != nil { + l.Errorw("[CreateCoupon] Database Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create coupon error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/coupon/deleteCouponLogic.go b/internal/logic/admin/coupon/deleteCouponLogic.go new file mode 100644 index 0000000..b041ecd --- /dev/null +++ b/internal/logic/admin/coupon/deleteCouponLogic.go @@ -0,0 +1,36 @@ +package coupon + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type DeleteCouponLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Delete coupon +func NewDeleteCouponLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteCouponLogic { + return &DeleteCouponLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteCouponLogic) DeleteCoupon(req *types.DeleteCouponRequest) error { + // delete coupon by id + err := l.svcCtx.CouponModel.Delete(l.ctx, req.Id) + if err != nil { + l.Errorw("[DeleteCoupon] Database Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete coupon error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/coupon/getCouponListLogic.go b/internal/logic/admin/coupon/getCouponListLogic.go new file mode 100644 index 0000000..c58e015 --- /dev/null +++ b/internal/logic/admin/coupon/getCouponListLogic.go @@ -0,0 +1,46 @@ +package coupon + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetCouponListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get coupon list +func NewGetCouponListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetCouponListLogic { + return &GetCouponListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetCouponListLogic) GetCouponList(req *types.GetCouponListRequest) (resp *types.GetCouponListResponse, err error) { + resp = &types.GetCouponListResponse{} + // get coupon list from db + total, list, err := l.svcCtx.CouponModel.QueryCouponListByPage(l.ctx, int(req.Page), int(req.Size), req.Subscribe, req.Search) + if err != nil { + l.Errorw("[GetCouponList] Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get coupon list error: %v", err.Error()) + } + resp.Total = total + resp.List = make([]types.Coupon, 0) + for _, coupon := range list { + couponInfo := types.Coupon{} + tool.DeepCopy(&couponInfo, coupon) + couponInfo.Subscribe = tool.StringToInt64Slice(coupon.Subscribe) + resp.List = append(resp.List, couponInfo) + } + return +} diff --git a/internal/logic/admin/coupon/updateCouponLogic.go b/internal/logic/admin/coupon/updateCouponLogic.go new file mode 100644 index 0000000..ee3136a --- /dev/null +++ b/internal/logic/admin/coupon/updateCouponLogic.go @@ -0,0 +1,43 @@ +package coupon + +import ( + "context" + "fmt" + + "github.com/perfect-panel/ppanel-server/internal/model/coupon" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateCouponLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Update coupon +func NewUpdateCouponLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateCouponLogic { + return &UpdateCouponLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateCouponLogic) UpdateCoupon(req *types.UpdateCouponRequest) error { + fmt.Printf("req Subscribe: %v\n", req.Subscribe) + couponInfo := &coupon.Coupon{} + // update coupon + tool.DeepCopy(couponInfo, req) + couponInfo.Subscribe = tool.Int64SliceToString(req.Subscribe) + err := l.svcCtx.CouponModel.Update(l.ctx, couponInfo) + if err != nil { + l.Errorw("[UpdateCoupon] Database Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update coupon error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/document/batchDeleteDocumentLogic.go b/internal/logic/admin/document/batchDeleteDocumentLogic.go new file mode 100644 index 0000000..3a53f46 --- /dev/null +++ b/internal/logic/admin/document/batchDeleteDocumentLogic.go @@ -0,0 +1,36 @@ +package document + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type BatchDeleteDocumentLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Batch delete document +func NewBatchDeleteDocumentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BatchDeleteDocumentLogic { + return &BatchDeleteDocumentLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *BatchDeleteDocumentLogic) BatchDeleteDocument(req *types.BatchDeleteDocumentRequest) error { + for _, id := range req.Ids { + if err := l.svcCtx.DocumentModel.Delete(l.ctx, id); err != nil { + l.Errorw("[BatchDeleteDocument] Database Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "failed to delete document: %v", err.Error()) + } + } + return nil +} diff --git a/internal/logic/admin/document/createDocumentLogic.go b/internal/logic/admin/document/createDocumentLogic.go new file mode 100644 index 0000000..f30a0cd --- /dev/null +++ b/internal/logic/admin/document/createDocumentLogic.go @@ -0,0 +1,41 @@ +package document + +import ( + "context" + "strings" + + "github.com/perfect-panel/ppanel-server/internal/model/document" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type CreateDocumentLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Create document +func NewCreateDocumentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateDocumentLogic { + return &CreateDocumentLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateDocumentLogic) CreateDocument(req *types.CreateDocumentRequest) error { + if err := l.svcCtx.DocumentModel.Insert(l.ctx, &document.Document{ + Title: req.Title, + Content: req.Content, + Tags: strings.Join(req.Tags, ","), + Show: req.Show, + }); err != nil { + l.Errorw("[CreateDocument] Database Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert document error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/document/deleteDocumentLogic.go b/internal/logic/admin/document/deleteDocumentLogic.go new file mode 100644 index 0000000..f264d05 --- /dev/null +++ b/internal/logic/admin/document/deleteDocumentLogic.go @@ -0,0 +1,34 @@ +package document + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type DeleteDocumentLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Delete document +func NewDeleteDocumentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteDocumentLogic { + return &DeleteDocumentLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteDocumentLogic) DeleteDocument(req *types.DeleteDocumentRequest) error { + if err := l.svcCtx.DocumentModel.Delete(l.ctx, req.Id); err != nil { + l.Errorw("[DeleteDocument] Database Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "failed to delete document: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/document/getDocumentDetailLogic.go b/internal/logic/admin/document/getDocumentDetailLogic.go new file mode 100644 index 0000000..7b94a61 --- /dev/null +++ b/internal/logic/admin/document/getDocumentDetailLogic.go @@ -0,0 +1,44 @@ +package document + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetDocumentDetailLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get document detail +func NewGetDocumentDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetDocumentDetailLogic { + return &GetDocumentDetailLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetDocumentDetailLogic) GetDocumentDetail(req *types.GetDocumentDetailRequest) (resp *types.Document, err error) { + data, err := l.svcCtx.DocumentModel.QueryDocumentDetail(l.ctx, req.Id) + if err != nil { + l.Errorw("[GetDocumentDetail] Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryDocumentDetail error: %v", err.Error()) + } + resp = &types.Document{ + Id: data.Id, + Title: data.Title, + Tags: tool.StringMergeAndRemoveDuplicates(data.Tags), + Content: data.Content, + CreatedAt: data.CreatedAt.UnixMilli(), + UpdatedAt: data.UpdatedAt.UnixMilli(), + } + return +} diff --git a/internal/logic/admin/document/getDocumentListLogic.go b/internal/logic/admin/document/getDocumentListLogic.go new file mode 100644 index 0000000..1aa88db --- /dev/null +++ b/internal/logic/admin/document/getDocumentListLogic.go @@ -0,0 +1,51 @@ +package document + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetDocumentListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get document list +func NewGetDocumentListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetDocumentListLogic { + return &GetDocumentListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetDocumentListLogic) GetDocumentList(req *types.GetDocumentListRequest) (resp *types.GetDocumentListResponse, err error) { + total, data, err := l.svcCtx.DocumentModel.QueryDocumentList(l.ctx, int(req.Page), int(req.Size), req.Tag, req.Search) + if err != nil { + l.Errorw("[GetDocumentList] Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryDocumentList error: %v", err.Error()) + } + resp = &types.GetDocumentListResponse{ + Total: total, + List: make([]types.Document, 0), + } + for _, v := range data { + resp.List = append(resp.List, types.Document{ + Id: v.Id, + Title: v.Title, + Tags: tool.StringMergeAndRemoveDuplicates(v.Tags), + Content: v.Content, + Show: *v.Show, + CreatedAt: v.CreatedAt.UnixMilli(), + UpdatedAt: v.UpdatedAt.UnixMilli(), + }) + } + return +} diff --git a/internal/logic/admin/document/updateDocumentLogic.go b/internal/logic/admin/document/updateDocumentLogic.go new file mode 100644 index 0000000..e741028 --- /dev/null +++ b/internal/logic/admin/document/updateDocumentLogic.go @@ -0,0 +1,42 @@ +package document + +import ( + "context" + "strings" + + "github.com/perfect-panel/ppanel-server/internal/model/document" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateDocumentLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Update document +func NewUpdateDocumentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateDocumentLogic { + return &UpdateDocumentLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateDocumentLogic) UpdateDocument(req *types.UpdateDocumentRequest) error { + if err := l.svcCtx.DocumentModel.Update(l.ctx, &document.Document{ + Id: req.Id, + Title: req.Title, + Content: req.Content, + Tags: strings.Join(req.Tags, ","), + Show: req.Show, + }); err != nil { + l.Errorw("[UpdateDocument] Database Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "failed to update document: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/log/getMessageLogListLogic.go b/internal/logic/admin/log/getMessageLogListLogic.go new file mode 100644 index 0000000..f8be0cb --- /dev/null +++ b/internal/logic/admin/log/getMessageLogListLogic.go @@ -0,0 +1,50 @@ +package log + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/log" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetMessageLogListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewGetMessageLogListLogic Get message log list +func NewGetMessageLogListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetMessageLogListLogic { + return &GetMessageLogListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetMessageLogListLogic) GetMessageLogList(req *types.GetMessageLogListRequest) (resp *types.GetMessageLogListResponse, err error) { + total, data, err := l.svcCtx.LogModel.FindMessageLogList(l.ctx, req.Page, req.Size, log.MessageLogFilterParams{ + Type: req.Type, + Platform: req.Platform, + To: req.To, + Subject: req.Subject, + Content: req.Content, + Status: req.Status, + }) + if err != nil { + l.Errorw("[GetMessageLogList] Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[GetMessageLogList] Database Error: %s", err.Error()) + } + var list []types.MessageLog + tool.DeepCopy(&list, data) + + return &types.GetMessageLogListResponse{ + Total: total, + List: list, + }, nil +} diff --git a/internal/logic/admin/order/createOrderLogic.go b/internal/logic/admin/order/createOrderLogic.go new file mode 100644 index 0000000..cc9036c --- /dev/null +++ b/internal/logic/admin/order/createOrderLogic.go @@ -0,0 +1,59 @@ +package order + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/order" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type CreateOrderLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Create order +func NewCreateOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateOrderLogic { + return &CreateOrderLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateOrderLogic) CreateOrder(req *types.CreateOrderRequest) error { + paymentMethod, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.PaymentId) + if err != nil { + l.Logger.Error("[CreateOrder] PaymentMethod Not Found", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.PaymentMethodNotFound), "PaymentMethod not found: %v", err.Error()) + } + + err = l.svcCtx.OrderModel.Insert(l.ctx, &order.Order{ + UserId: req.UserId, + OrderNo: tool.GenerateTradeNo(), + Type: req.Type, + Quantity: req.Quantity, + Price: req.Price, + Amount: req.Amount, + Discount: req.Discount, + Coupon: req.Coupon, + CouponDiscount: req.CouponDiscount, + PaymentId: req.PaymentId, + Method: paymentMethod.Token, + FeeAmount: req.FeeAmount, + TradeNo: req.TradeNo, + Status: req.Status, + SubscribeId: req.SubscribeId, + }) + if err != nil { + l.Logger.Error("[CreateOrder] Database Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "Insert error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/order/getOrderListLogic.go b/internal/logic/admin/order/getOrderListLogic.go new file mode 100644 index 0000000..e272b15 --- /dev/null +++ b/internal/logic/admin/order/getOrderListLogic.go @@ -0,0 +1,40 @@ +package order + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetOrderListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewGetOrderListLogic Get order list +func NewGetOrderListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetOrderListLogic { + return &GetOrderListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetOrderListLogic) GetOrderList(req *types.GetOrderListRequest) (resp *types.GetOrderListResponse, err error) { + total, list, err := l.svcCtx.OrderModel.QueryOrderListByPage(l.ctx, int(req.Page), int(req.Size), req.Status, req.UserId, req.SubscribeId, req.Search) + if err != nil { + l.Errorw("[GetOrderList] Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryOrderListByPage error: %v", err.Error()) + } + resp = &types.GetOrderListResponse{} + resp.List = make([]types.Order, 0) + tool.DeepCopy(&resp.List, list) + resp.Total = total + return +} diff --git a/internal/logic/admin/order/updateOrderStatusLogic.go b/internal/logic/admin/order/updateOrderStatusLogic.go new file mode 100644 index 0000000..c82a8a0 --- /dev/null +++ b/internal/logic/admin/order/updateOrderStatusLogic.go @@ -0,0 +1,81 @@ +package order + +import ( + "context" + "encoding/json" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + queue "github.com/perfect-panel/ppanel-server/queue/types" +) + +type UpdateOrderStatusLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Update order status +func NewUpdateOrderStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateOrderStatusLogic { + return &UpdateOrderStatusLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateOrderStatusLogic) UpdateOrderStatus(req *types.UpdateOrderStatusRequest) error { + info, err := l.svcCtx.OrderModel.FindOne(l.ctx, req.Id) + if err != nil { + l.Errorw("[UpdateOrderStatus] FindOne error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOne error: %v", err.Error()) + } + + if req.PaymentId != 0 { + paymentMethod, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.PaymentId) + if err != nil { + l.Logger.Error("[CreateOrder] PaymentMethod Not Found", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.PaymentMethodNotFound), "PaymentMethod not found: %v", err.Error()) + } + info.PaymentId = paymentMethod.Id + info.Method = paymentMethod.Platform + } + if req.TradeNo != "" { + info.TradeNo = req.TradeNo + } + + err = l.svcCtx.OrderModel.Transaction(l.ctx, func(db *gorm.DB) error { + if err := l.svcCtx.OrderModel.Update(l.ctx, info, db); err != nil { + l.Errorw("[UpdateOrderStatus] Update error", logger.Field("error", err.Error()), logger.Field("OrderID", info.Id)) + return err + } + if err := l.svcCtx.OrderModel.UpdateOrderStatus(l.ctx, info.OrderNo, req.Status, db); err != nil { + return err + } + // If order status is 2, create user subscription + if req.Status == 2 { + payload := queue.ForthwithActivateOrderPayload{ + OrderNo: info.OrderNo, + } + p, _ := json.Marshal(payload) + task := asynq.NewTask(queue.ForthwithActivateOrder, p) + _, err = l.svcCtx.Queue.EnqueueContext(l.ctx, task) + if err != nil { + l.Errorw("[UpdateOrderStatus] Enqueue error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.QueueEnqueueError), "Enqueue error: %v", err.Error()) + } + } + return nil + }) + if err != nil { + l.Errorw("[UpdateOrderStatus] Transaction error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Transaction error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/payment/createPaymentMethodLogic.go b/internal/logic/admin/payment/createPaymentMethodLogic.go new file mode 100644 index 0000000..1a132cf --- /dev/null +++ b/internal/logic/admin/payment/createPaymentMethodLogic.go @@ -0,0 +1,97 @@ +package payment + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/ppanel-server/pkg/random" + + paymentModel "github.com/perfect-panel/ppanel-server/internal/model/payment" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/payment" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type CreatePaymentMethodLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewCreatePaymentMethodLogic Create Payment Method +func NewCreatePaymentMethodLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreatePaymentMethodLogic { + return &CreatePaymentMethodLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreatePaymentMethodLogic) CreatePaymentMethod(req *types.CreatePaymentMethodRequest) (resp *types.PaymentConfig, err error) { + if payment.ParsePlatform(req.Platform) == payment.UNSUPPORTED { + l.Errorw("unsupported payment platform", logger.Field("mark", req.Platform)) + return nil, errors.Wrapf(xerr.NewErrCodeMsg(400, "UNSUPPORTED_PAYMENT_PLATFORM"), "unsupported payment platform: %s", req.Platform) + } + config := parsePaymentPlatformConfig(l.ctx, payment.ParsePlatform(req.Platform), req.Config) + var paymentMethod = &paymentModel.Payment{ + Name: req.Name, + Platform: req.Platform, + Icon: req.Icon, + Domain: req.Domain, + Description: req.Description, + Config: config, + FeeMode: req.FeeMode, + FeePercent: req.FeePercent, + FeeAmount: req.FeeAmount, + Enable: req.Enable, + Token: random.KeyNew(8, 1), + } + if err := l.svcCtx.PaymentModel.Insert(l.ctx, paymentMethod); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert payment method error: %s", err.Error()) + } + resp = &types.PaymentConfig{} + tool.DeepCopy(resp, paymentMethod) + var configMap map[string]interface{} + _ = json.Unmarshal([]byte(paymentMethod.Config), &configMap) + resp.Config = configMap + return +} + +func parsePaymentPlatformConfig(ctx context.Context, platform payment.Platform, config interface{}) string { + data, err := json.Marshal(config) + if err != nil { + logger.WithContext(ctx).Errorw("parse payment platform config error", logger.Field("platform", platform), logger.Field("config", config), logger.Field("error", err.Error())) + } + switch platform { + case payment.Stripe: + stripe := &paymentModel.StripeConfig{} + if err := stripe.Unmarshal(string(data)); err != nil { + logger.WithContext(ctx).Errorw("parse stripe config error", logger.Field("config", string(data)), logger.Field("error", err.Error())) + } + return stripe.Marshal() + case payment.AlipayF2F: + alipay := &paymentModel.AlipayF2FConfig{} + if err := alipay.Unmarshal(string(data)); err != nil { + logger.WithContext(ctx).Errorw("parse alipay config error", logger.Field("config", string(data)), logger.Field("error", err.Error())) + } + return alipay.Marshal() + case payment.EPay: + epay := &paymentModel.EPayConfig{} + if err := epay.Unmarshal(string(data)); err != nil { + logger.WithContext(ctx).Errorw("parse epay config error", logger.Field("config", string(data)), logger.Field("error", err.Error())) + } + return epay.Marshal() + case payment.Payssion: + payssion := &paymentModel.PayssionConfig{} + if err := payssion.Unmarshal(string(data)); err != nil { + logger.WithContext(ctx).Errorw("parse payssion config error", logger.Field("config", string(data)), logger.Field("error", err.Error())) + } + return payssion.Marshal() + default: + return "" + } +} diff --git a/internal/logic/admin/payment/deletePaymentMethodLogic.go b/internal/logic/admin/payment/deletePaymentMethodLogic.go new file mode 100644 index 0000000..72b4075 --- /dev/null +++ b/internal/logic/admin/payment/deletePaymentMethodLogic.go @@ -0,0 +1,34 @@ +package payment + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type DeletePaymentMethodLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Delete Payment Method +func NewDeletePaymentMethodLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeletePaymentMethodLogic { + return &DeletePaymentMethodLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeletePaymentMethodLogic) DeletePaymentMethod(req *types.DeletePaymentMethodRequest) error { + if err := l.svcCtx.PaymentModel.Delete(l.ctx, req.Id); err != nil { + l.Errorw("delete payment method error", logger.Field("id", req.Id), logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete payment method error: %s", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/payment/getPaymentMethodListLogic.go b/internal/logic/admin/payment/getPaymentMethodListLogic.go new file mode 100644 index 0000000..2b9cbf5 --- /dev/null +++ b/internal/logic/admin/payment/getPaymentMethodListLogic.go @@ -0,0 +1,72 @@ +package payment + +import ( + "context" + "encoding/json" + + paymentPlatform "github.com/perfect-panel/ppanel-server/pkg/payment" + + "github.com/perfect-panel/ppanel-server/internal/model/payment" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetPaymentMethodListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewGetPaymentMethodListLogic Get Payment Method List +func NewGetPaymentMethodListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPaymentMethodListLogic { + return &GetPaymentMethodListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetPaymentMethodListLogic) GetPaymentMethodList(req *types.GetPaymentMethodListRequest) (resp *types.GetPaymentMethodListResponse, err error) { + total, list, err := l.svcCtx.PaymentModel.FindListByPage(l.ctx, req.Page, req.Size, &payment.Filter{ + Search: req.Search, + Mark: req.Platform, + Enable: req.Enable, + }) + if err != nil { + l.Errorw("find payment method list error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method list error: %s", err.Error()) + } + resp = &types.GetPaymentMethodListResponse{ + Total: total, + List: make([]types.PaymentMethodDetail, len(list)), + } + for i, v := range list { + config := make(map[string]interface{}) + _ = json.Unmarshal([]byte(v.Config), &config) + notifyUrl := "" + if paymentPlatform.ParsePlatform(v.Platform) != paymentPlatform.Balance { + if v.Domain != "" { + notifyUrl = v.Domain + "/v1/notify/" + v.Platform + "/" + v.Token + } else { + notifyUrl = "https://" + l.svcCtx.Config.Host + "/v1/notify/" + v.Platform + "/" + v.Token + } + } + resp.List[i] = types.PaymentMethodDetail{ + Id: v.Id, + Name: v.Name, + Platform: v.Platform, + Icon: v.Icon, + Domain: v.Domain, + Config: config, + FeeMode: v.FeeMode, + FeePercent: v.FeePercent, + FeeAmount: v.FeeAmount, + Enable: *v.Enable, + NotifyURL: notifyUrl, + } + } + return +} diff --git a/internal/logic/admin/payment/getPaymentPlatformLogic.go b/internal/logic/admin/payment/getPaymentPlatformLogic.go new file mode 100644 index 0000000..e04cac8 --- /dev/null +++ b/internal/logic/admin/payment/getPaymentPlatformLogic.go @@ -0,0 +1,32 @@ +package payment + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/payment" +) + +type GetPaymentPlatformLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get supported payment platform +func NewGetPaymentPlatformLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPaymentPlatformLogic { + return &GetPaymentPlatformLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetPaymentPlatformLogic) GetPaymentPlatform() (resp *types.PlatformResponse, err error) { + resp = &types.PlatformResponse{ + List: payment.GetSupportedPlatforms(), + } + return +} diff --git a/internal/logic/admin/payment/updatePaymentMethodLogic.go b/internal/logic/admin/payment/updatePaymentMethodLogic.go new file mode 100644 index 0000000..25ff600 --- /dev/null +++ b/internal/logic/admin/payment/updatePaymentMethodLogic.go @@ -0,0 +1,54 @@ +package payment + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/payment" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdatePaymentMethodLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Update Payment Method +func NewUpdatePaymentMethodLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdatePaymentMethodLogic { + return &UpdatePaymentMethodLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdatePaymentMethodLogic) UpdatePaymentMethod(req *types.UpdatePaymentMethodRequest) (resp *types.PaymentConfig, err error) { + if payment.ParsePlatform(req.Platform) == payment.UNSUPPORTED { + l.Errorw("unsupported payment platform", logger.Field("mark", req.Platform)) + return nil, errors.Wrapf(xerr.NewErrCodeMsg(400, "UNSUPPORTED_PAYMENT_PLATFORM"), "unsupported payment platform: %s", req.Platform) + } + method, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Id) + if err != nil { + l.Errorw("find payment method error", logger.Field("id", req.Id), logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %s", err.Error()) + } + config := parsePaymentPlatformConfig(l.ctx, payment.ParsePlatform(req.Platform), req.Config) + tool.DeepCopy(method, req) + method.Config = config + if err := l.svcCtx.PaymentModel.Update(l.ctx, method); err != nil { + l.Errorw("update payment method error", logger.Field("id", req.Id), logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update payment method error: %s", err.Error()) + } + resp = &types.PaymentConfig{} + tool.DeepCopy(resp, method) + var configMap map[string]interface{} + _ = json.Unmarshal([]byte(method.Config), &configMap) + resp.Config = configMap + return +} diff --git a/internal/logic/admin/server/batchDeleteNodeGroupLogic.go b/internal/logic/admin/server/batchDeleteNodeGroupLogic.go new file mode 100644 index 0000000..83f1201 --- /dev/null +++ b/internal/logic/admin/server/batchDeleteNodeGroupLogic.go @@ -0,0 +1,44 @@ +package server + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type BatchDeleteNodeGroupLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewBatchDeleteNodeGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BatchDeleteNodeGroupLogic { + return &BatchDeleteNodeGroupLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *BatchDeleteNodeGroupLogic) BatchDeleteNodeGroup(req *types.BatchDeleteNodeGroupRequest) error { + // Check if the group is empty + count, err := l.svcCtx.ServerModel.QueryServerCountByServerGroups(l.ctx, req.Ids) + if err != nil { + l.Errorw("[BatchDeleteNodeGroup] Query Database Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query server error: %v", err) + } + if count > 0 { + return errors.Wrapf(xerr.NewErrCode(xerr.NodeGroupNotEmpty), "group is not empty") + } + // Delete the group + err = l.svcCtx.ServerModel.BatchDeleteNodeGroup(l.ctx, req.Ids) + if err != nil { + l.Errorw("[BatchDeleteNodeGroup] Delete Database Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), err.Error()) + } + return nil +} diff --git a/internal/logic/admin/server/batchDeleteNodeLogic.go b/internal/logic/admin/server/batchDeleteNodeLogic.go new file mode 100644 index 0000000..4cfcb95 --- /dev/null +++ b/internal/logic/admin/server/batchDeleteNodeLogic.go @@ -0,0 +1,43 @@ +package server + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type BatchDeleteNodeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewBatchDeleteNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BatchDeleteNodeLogic { + return &BatchDeleteNodeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *BatchDeleteNodeLogic) BatchDeleteNode(req *types.BatchDeleteNodeRequest) error { + err := l.svcCtx.DB.Transaction(func(db *gorm.DB) error { + for _, id := range req.Ids { + err := l.svcCtx.ServerModel.Delete(l.ctx, id) + if err != nil { + return err + } + } + return nil + }) + if err != nil { + l.Errorw("[BatchDeleteNode] Delete Database Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), err.Error()) + } + return nil +} diff --git a/internal/logic/admin/server/createNodeGroupLogic.go b/internal/logic/admin/server/createNodeGroupLogic.go new file mode 100644 index 0000000..83a8d71 --- /dev/null +++ b/internal/logic/admin/server/createNodeGroupLogic.go @@ -0,0 +1,40 @@ +package server + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/server" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + + "github.com/pkg/errors" +) + +type CreateNodeGroupLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewCreateNodeGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateNodeGroupLogic { + return &CreateNodeGroupLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateNodeGroupLogic) CreateNodeGroup(req *types.CreateNodeGroupRequest) error { + groupInfo := &server.Group{ + Name: req.Name, + Description: req.Description, + } + err := l.svcCtx.ServerModel.InsertGroup(l.ctx, groupInfo) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), err.Error()) + } + return nil +} diff --git a/internal/logic/admin/server/createNodeLogic.go b/internal/logic/admin/server/createNodeLogic.go new file mode 100644 index 0000000..3e87ce9 --- /dev/null +++ b/internal/logic/admin/server/createNodeLogic.go @@ -0,0 +1,106 @@ +package server + +import ( + "context" + "encoding/json" + "strings" + "time" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/model/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + queue "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/pkg/errors" +) + +type CreateNodeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewCreateNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateNodeLogic { + return &CreateNodeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateNodeLogic) CreateNode(req *types.CreateNodeRequest) error { + config, err := json.Marshal(req.Config) + if err != nil { + return err + } + var serverInfo server.Server + tool.DeepCopy(&serverInfo, req) + serverInfo.Config = string(config) + nodeRelay, err := json.Marshal(req.RelayNode) + if err != nil { + l.Errorw("[UpdateNode] Marshal RelayNode Error: ", logger.Field("error", err.Error())) + return err + } + if len(req.Tags) > 0 { + serverInfo.Tags = strings.Join(req.Tags, ",") + } + + serverInfo.LastReportedAt = time.UnixMicro(1218124800) + + serverInfo.City = req.City + serverInfo.Country = req.Country + + serverInfo.RelayNode = string(nodeRelay) + if req.Protocol == "vless" { + var cfg types.Vless + if err := json.Unmarshal(config, &cfg); err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "json.Unmarshal error: %v", err.Error()) + } + if cfg.Security == "reality" && cfg.SecurityConfig.RealityPublicKey == "" { + public, private, err := tool.Curve25519Genkey(false, "") + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "generate curve25519 key error") + } + cfg.SecurityConfig.RealityPublicKey = public + cfg.SecurityConfig.RealityPrivateKey = private + cfg.SecurityConfig.RealityShortId = tool.GenerateShortID(private) + } + if cfg.SecurityConfig.RealityServerAddr == "" { + cfg.SecurityConfig.RealityServerAddr = cfg.SecurityConfig.SNI + } + if cfg.SecurityConfig.RealityServerPort == 0 { + cfg.SecurityConfig.RealityServerPort = 443 + } + config, _ = json.Marshal(cfg) + serverInfo.Config = string(config) + } + + err = l.svcCtx.ServerModel.Insert(l.ctx, &serverInfo) + if err != nil { + l.Errorw("[CreateNode] Insert Database Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create server error: %v", err) + } + + // Marshal the task payload + payload, err := json.Marshal(queue.GetNodeCountry{ + Protocol: serverInfo.Protocol, + ServerAddr: serverInfo.ServerAddr, + }) + if err != nil { + l.Errorw("[GetNodeCountry]: Marshal Error", logger.Field("error", err.Error())) + return errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to marshal task payload") + } + // Create a queue task + task := asynq.NewTask(queue.ForthwithGetCountry, payload) + // Enqueue the task + taskInfo, err := l.svcCtx.Queue.Enqueue(task) + if err != nil { + l.Errorw("[GetNodeCountry]: Enqueue Error", logger.Field("error", err.Error()), logger.Field("payload", string(payload))) + return errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to enqueue task") + } + l.Infow("[GetNodeCountry]: Enqueue Success", logger.Field("taskID", taskInfo.ID), logger.Field("payload", string(payload))) + return nil +} diff --git a/internal/logic/admin/server/createRuleGroupLogic.go b/internal/logic/admin/server/createRuleGroupLogic.go new file mode 100644 index 0000000..d8bb8bb --- /dev/null +++ b/internal/logic/admin/server/createRuleGroupLogic.go @@ -0,0 +1,69 @@ +package server + +import ( + "context" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/rules" + + "github.com/perfect-panel/ppanel-server/internal/model/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type CreateRuleGroupLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Create rule group +func NewCreateRuleGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateRuleGroupLogic { + return &CreateRuleGroupLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} +func parseAndValidateRules(ruleText, ruleName string) ([]string, error) { + var rs []string + ruleArr := strings.Split(ruleText, "\n") + if len(ruleArr) == 0 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "rules is empty") + } + + for _, s := range ruleArr { + r := rules.NewRule(s, ruleName) + if r == nil { + continue + } + if err := r.Validate(); err != nil { + continue + } + rs = append(rs, r.String()) + } + return rs, nil +} +func (l *CreateRuleGroupLogic) CreateRuleGroup(req *types.CreateRuleGroupRequest) error { + rs, err := parseAndValidateRules(req.Rules, req.Name) + if err != nil { + return err + } + + err = l.svcCtx.ServerModel.InsertRuleGroup(l.ctx, &server.RuleGroup{ + Name: req.Name, + Icon: req.Icon, + Tags: tool.StringSliceToString(req.Tags), + Rules: strings.Join(rs, "\n"), + Enable: req.Enable, + }) + if err != nil { + l.Errorw("[CreateRuleGroup] Insert Database Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create server rule group error: %v", err) + } + return nil +} diff --git a/internal/logic/admin/server/deleteNodeGroupLogic.go b/internal/logic/admin/server/deleteNodeGroupLogic.go new file mode 100644 index 0000000..dc1bac3 --- /dev/null +++ b/internal/logic/admin/server/deleteNodeGroupLogic.go @@ -0,0 +1,44 @@ +package server + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type DeleteNodeGroupLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewDeleteNodeGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteNodeGroupLogic { + return &DeleteNodeGroupLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteNodeGroupLogic) DeleteNodeGroup(req *types.DeleteNodeGroupRequest) error { + // Check if the group is empty + count, err := l.svcCtx.ServerModel.QueryServerCountByServerGroups(l.ctx, []int64{req.Id}) + if err != nil { + l.Errorw("[DeleteNodeGroup] Query Database Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query server error: %v", err) + } + if count > 0 { + return errors.Wrapf(xerr.NewErrCode(xerr.NodeGroupNotEmpty), "group is not empty") + } + // Delete the group + err = l.svcCtx.ServerModel.DeleteGroup(l.ctx, req.Id) + if err != nil { + l.Errorw("[DeleteNodeGroup] Delete Database Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), err.Error()) + } + return nil +} diff --git a/internal/logic/admin/server/deleteNodeLogic.go b/internal/logic/admin/server/deleteNodeLogic.go new file mode 100644 index 0000000..5c5655c --- /dev/null +++ b/internal/logic/admin/server/deleteNodeLogic.go @@ -0,0 +1,56 @@ +package server + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type DeleteNodeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewDeleteNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteNodeLogic { + return &DeleteNodeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteNodeLogic) DeleteNode(req *types.DeleteNodeRequest) error { + err := l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { + // Delete server + err := l.svcCtx.ServerModel.Delete(l.ctx, req.Id) + if err != nil { + return err + } + // Delete server to subscribe + subs, err := l.svcCtx.SubscribeModel.QuerySubscribeIdsByServerIdAndServerGroupId(l.ctx, req.Id, 0) + if err != nil { + return err + } + for _, sub := range subs { + servers := tool.StringToInt64Slice(sub.Server) + newServers := tool.RemoveElementBySlice(servers, req.Id) + sub.Server = tool.Int64SliceToString(newServers) + if err = l.svcCtx.SubscribeModel.Update(l.ctx, sub); err != nil { + return err + } + } + return nil + }) + if err != nil { + l.Errorw("[DeleteNode] Delete Database Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete server error: %v", err) + } + return nil +} diff --git a/internal/logic/admin/server/deleteRuleGroupLogic.go b/internal/logic/admin/server/deleteRuleGroupLogic.go new file mode 100644 index 0000000..63c7acb --- /dev/null +++ b/internal/logic/admin/server/deleteRuleGroupLogic.go @@ -0,0 +1,35 @@ +package server + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type DeleteRuleGroupLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Delete rule group +func NewDeleteRuleGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteRuleGroupLogic { + return &DeleteRuleGroupLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteRuleGroupLogic) DeleteRuleGroup(req *types.DeleteRuleGroupRequest) error { + err := l.svcCtx.ServerModel.DeleteRuleGroup(l.ctx, req.Id) + if err != nil { + l.Errorw("[DeleteRuleGroup] Delete Database Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete server rule group error: %v", err) + } + return nil +} diff --git a/internal/logic/admin/server/getNodeDetailLogic.go b/internal/logic/admin/server/getNodeDetailLogic.go new file mode 100644 index 0000000..c71d0be --- /dev/null +++ b/internal/logic/admin/server/getNodeDetailLogic.go @@ -0,0 +1,43 @@ +package server + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetNodeDetailLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetNodeDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetNodeDetailLogic { + return &GetNodeDetailLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetNodeDetailLogic) GetNodeDetail(req *types.GetDetailRequest) (resp *types.Server, err error) { + detail, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get server detail error: %v", err.Error()) + } + resp = &types.Server{} + tool.DeepCopy(resp, detail) + var cfg map[string]interface{} + err = json.Unmarshal([]byte(detail.Config), &cfg) + if err != nil { + cfg = make(map[string]interface{}) + } + resp.Config = cfg + return +} diff --git a/internal/logic/admin/server/getNodeGroupListLogic.go b/internal/logic/admin/server/getNodeGroupListLogic.go new file mode 100644 index 0000000..4a5cc32 --- /dev/null +++ b/internal/logic/admin/server/getNodeGroupListLogic.go @@ -0,0 +1,39 @@ +package server + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetNodeGroupListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetNodeGroupListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetNodeGroupListLogic { + return &GetNodeGroupListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetNodeGroupListLogic) GetNodeGroupList() (resp *types.GetNodeGroupListResponse, err error) { + nodeGroupList, err := l.svcCtx.ServerModel.QueryAllGroup(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error()) + } + nodeGroups := make([]types.ServerGroup, 0) + tool.DeepCopy(&nodeGroups, nodeGroupList) + return &types.GetNodeGroupListResponse{ + Total: int64(len(nodeGroups)), + List: nodeGroups, + }, nil +} diff --git a/internal/logic/admin/server/getNodeListLogic.go b/internal/logic/admin/server/getNodeListLogic.go new file mode 100644 index 0000000..1e04be6 --- /dev/null +++ b/internal/logic/admin/server/getNodeListLogic.go @@ -0,0 +1,100 @@ +package server + +import ( + "context" + "encoding/json" + "strings" + + "github.com/perfect-panel/ppanel-server/internal/model/server" + + "github.com/redis/go-redis/v9" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetNodeListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetNodeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetNodeListLogic { + return &GetNodeListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetNodeListLogic) GetNodeList(req *types.GetNodeServerListRequest) (resp *types.GetNodeServerListResponse, err error) { + total, list, err := l.svcCtx.ServerModel.FindServerListByFilter(l.ctx, &server.ServerFilter{ + Page: req.Page, + Size: req.Size, + Search: req.Search, + Tag: req.Tag, + Group: req.GroupId, + }) + if err != nil { + l.Errorw("[GetNodeList] Query Database Error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error()) + } + nodes := make([]types.Server, 0) + for _, v := range list { + node := types.Server{} + tool.DeepCopy(&node, v) + // default relay mode + if node.RelayMode == "" { + node.RelayMode = "none" + } + if len(v.Tags) > 0 { + if strings.Contains(v.Tags, ",") { + node.Tags = strings.Split(v.Tags, ",") + } else { + node.Tags = []string{v.Tags} + } + } + // parse config + var cfg map[string]interface{} + err = json.Unmarshal([]byte(v.Config), &cfg) + if err != nil { + cfg = make(map[string]interface{}) + } + node.Config = cfg + relayNode := make([]types.NodeRelay, 0) + err = json.Unmarshal([]byte(v.RelayNode), &relayNode) + if err != nil { + l.Errorw("[GetNodeList] Unmarshal RelayNode Error: ", logger.Field("error", err.Error()), logger.Field("relayNode", v.RelayNode)) + } + node.RelayNode = relayNode + var status types.NodeStatus + nodeStatus, err := l.svcCtx.NodeCache.GetNodeStatus(l.ctx, v.Id) + if err != nil { + // redis nil is not a Error + if !errors.Is(err, redis.Nil) { + l.Errorw("[GetNodeList] Get Node Status Error: ", logger.Field("error", err.Error())) + } + } else { + onlineUser, err := l.svcCtx.NodeCache.GetNodeOnlineUser(l.ctx, v.Id) + if err != nil { + l.Errorw("[GetNodeList] Get Node Online User Error: ", logger.Field("error", err.Error())) + } else { + status.Online = onlineUser + } + status.Cpu = nodeStatus.Cpu + status.Mem = nodeStatus.Mem + status.Disk = nodeStatus.Disk + status.UpdatedAt = nodeStatus.UpdatedAt + } + node.Status = &status + nodes = append(nodes, node) + } + return &types.GetNodeServerListResponse{ + Total: total, + List: nodes, + }, nil +} diff --git a/internal/logic/admin/server/getNodeTagListLogic.go b/internal/logic/admin/server/getNodeTagListLogic.go new file mode 100644 index 0000000..722fd6d --- /dev/null +++ b/internal/logic/admin/server/getNodeTagListLogic.go @@ -0,0 +1,53 @@ +package server + +import ( + "context" + "strings" + + "github.com/perfect-panel/ppanel-server/internal/model/server" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type GetNodeTagListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get node tag list +func NewGetNodeTagListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetNodeTagListLogic { + return &GetNodeTagListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetNodeTagListLogic) GetNodeTagList() (resp *types.GetNodeTagListResponse, err error) { + var nodeTags, tags []string + err = l.svcCtx.ServerModel.Transaction(l.ctx, func(db *gorm.DB) error { + + return db.Model(&server.Server{}).Select("tags").Pluck("tags", &nodeTags).Error + }) + + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get node tag list failed, %s", err.Error()) + } + + for _, tag := range nodeTags { + tags = append(tags, strings.Split(tag, ",")...) + } + + // Remove duplicate tags + tags = tool.RemoveDuplicateElements(tags...) + + return &types.GetNodeTagListResponse{ + Tags: tags, + }, nil +} diff --git a/internal/logic/admin/server/getRuleGroupListLogic.go b/internal/logic/admin/server/getRuleGroupListLogic.go new file mode 100644 index 0000000..a22766d --- /dev/null +++ b/internal/logic/admin/server/getRuleGroupListLogic.go @@ -0,0 +1,52 @@ +package server + +import ( + "context" + "strings" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetRuleGroupListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get rule group list +func NewGetRuleGroupListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetRuleGroupListLogic { + return &GetRuleGroupListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetRuleGroupListLogic) GetRuleGroupList() (resp *types.GetRuleGroupResponse, err error) { + nodeRuleGroupList, err := l.svcCtx.ServerModel.QueryAllRuleGroup(l.ctx) + if err != nil { + l.Errorw("[GetRuleGroupList] Query Database Error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error()) + } + nodeRuleGroups := make([]types.ServerRuleGroup, len(nodeRuleGroupList)) + for i, v := range nodeRuleGroupList { + nodeRuleGroups[i] = types.ServerRuleGroup{ + Id: v.Id, + Icon: v.Icon, + Name: v.Name, + Tags: strings.Split(v.Tags, ","), + Rules: v.Rules, + Enable: v.Enable, + CreatedAt: v.CreatedAt.UnixMilli(), + UpdatedAt: v.UpdatedAt.UnixMilli(), + } + } + return &types.GetRuleGroupResponse{ + Total: int64(len(nodeRuleGroups)), + List: nodeRuleGroups, + }, nil +} diff --git a/internal/logic/admin/server/nodeSortLogic.go b/internal/logic/admin/server/nodeSortLogic.go new file mode 100644 index 0000000..f6c8842 --- /dev/null +++ b/internal/logic/admin/server/nodeSortLogic.go @@ -0,0 +1,82 @@ +package server + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/server" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type NodeSortLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Node sort +func NewNodeSortLogic(ctx context.Context, svcCtx *svc.ServiceContext) *NodeSortLogic { + return &NodeSortLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *NodeSortLogic) NodeSort(req *types.NodeSortRequest) error { + err := l.svcCtx.ServerModel.Transaction(l.ctx, func(db *gorm.DB) error { + // find all servers id + var existingIDs []int64 + db.Model(&server.Server{}).Select("id").Find(&existingIDs) + // check if the id is valid + validIDMap := make(map[int64]bool) + for _, id := range existingIDs { + validIDMap[id] = true + } + // check if the sort is valid + var validItems []types.SortItem + for _, item := range req.Sort { + if validIDMap[item.Id] { + validItems = append(validItems, item) + } + } + // query all servers + var servers []*server.Server + db.Model(&server.Server{}).Order("sort ASC").Find(&servers) + // create a map of the current sort + currentSortMap := make(map[int64]int64) + for _, item := range servers { + currentSortMap[item.Id] = item.Sort + } + + // new sort map + newSortMap := make(map[int64]int64) + for _, item := range validItems { + newSortMap[item.Id] = item.Sort + } + + var itemsToUpdate []types.SortItem + for _, item := range validItems { + if oldSort, exists := currentSortMap[item.Id]; exists && oldSort != item.Sort { + itemsToUpdate = append(itemsToUpdate, item) + } + } + for _, item := range itemsToUpdate { + if err := db.Model(&server.Server{}).Where("id = ?", item.Id).Update("sort", item.Sort).Error; err != nil { + l.Errorw("[NodeSort] Update Database Error: ", logger.Field("error", err.Error()), logger.Field("id", item.Id), logger.Field("sort", item.Sort)) + return err + } + } + return nil + }) + if err != nil { + l.Errorw("[NodeSort] Update Database Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), err.Error()) + } + return nil +} diff --git a/internal/logic/admin/server/updateNodeGroupLogic.go b/internal/logic/admin/server/updateNodeGroupLogic.go new file mode 100644 index 0000000..6774c0b --- /dev/null +++ b/internal/logic/admin/server/updateNodeGroupLogic.go @@ -0,0 +1,40 @@ +package server + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateNodeGroupLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUpdateNodeGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateNodeGroupLogic { + return &UpdateNodeGroupLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateNodeGroupLogic) UpdateNodeGroup(req *types.UpdateNodeGroupRequest) error { + // check server group exist + nodeGroup, err := l.svcCtx.ServerModel.FindOneGroup(l.ctx, req.Id) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error()) + } + nodeGroup.Name = req.Name + nodeGroup.Description = req.Description + err = l.svcCtx.ServerModel.UpdateGroup(l.ctx, nodeGroup) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), err.Error()) + } + return nil +} diff --git a/internal/logic/admin/server/updateNodeLogic.go b/internal/logic/admin/server/updateNodeLogic.go new file mode 100644 index 0000000..c5dc4ff --- /dev/null +++ b/internal/logic/admin/server/updateNodeLogic.go @@ -0,0 +1,110 @@ +package server + +import ( + "context" + "encoding/json" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/device" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + queue "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/pkg/errors" +) + +type UpdateNodeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUpdateNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateNodeLogic { + return &UpdateNodeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateNodeLogic) UpdateNode(req *types.UpdateNodeRequest) error { + // Check server exist + nodeInfo, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.Id) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find server error: %v", err) + } + tool.DeepCopy(nodeInfo, req) + config, err := json.Marshal(req.Config) + if err != nil { + return err + } + + nodeInfo.Config = string(config) + nodeRelay, err := json.Marshal(req.RelayNode) + if err != nil { + l.Errorw("[UpdateNode] Marshal RelayNode Error: ", logger.Field("error", err.Error())) + return err + } + + if len(req.Tags) > 0 { + nodeInfo.Tags = strings.Join(req.Tags, ",") + } + + nodeInfo.City = req.City + nodeInfo.Country = req.Country + + nodeInfo.RelayNode = string(nodeRelay) + if req.Protocol == "vless" { + var cfg types.Vless + if err := json.Unmarshal(config, &cfg); err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "json.Unmarshal error: %v", err.Error()) + } + if cfg.Security == "reality" && cfg.SecurityConfig.RealityPublicKey == "" { + public, private, err := tool.Curve25519Genkey(false, "") + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "generate curve25519 key error") + } + cfg.SecurityConfig.RealityPublicKey = public + cfg.SecurityConfig.RealityPrivateKey = private + cfg.SecurityConfig.RealityShortId = tool.GenerateShortID(private) + } + if cfg.SecurityConfig.RealityServerAddr == "" { + cfg.SecurityConfig.RealityServerAddr = cfg.SecurityConfig.SNI + } + if cfg.SecurityConfig.RealityServerPort == 0 { + cfg.SecurityConfig.RealityServerPort = 443 + } + config, _ = json.Marshal(cfg) + nodeInfo.Config = string(config) + } + err = l.svcCtx.ServerModel.Update(l.ctx, nodeInfo) + if err != nil { + l.Errorw("[UpdateNode] Update Database Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create server error: %v", err) + } + + // Marshal the task payload + payload, err := json.Marshal(queue.GetNodeCountry{ + Protocol: nodeInfo.Protocol, + ServerAddr: nodeInfo.ServerAddr, + }) + if err != nil { + l.Errorw("[GetNodeCountry]: Marshal Error", logger.Field("error", err.Error())) + return errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to marshal task payload") + } + // Create a queue task + task := asynq.NewTask(queue.ForthwithGetCountry, payload) + // Enqueue the task + taskInfo, err := l.svcCtx.Queue.Enqueue(task) + if err != nil { + l.Errorw("[GetNodeCountry]: Enqueue Error", logger.Field("error", err.Error()), logger.Field("payload", string(payload))) + return errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to enqueue task") + } + l.Infow("[GetNodeCountry]: Enqueue Success", logger.Field("taskID", taskInfo.ID), logger.Field("payload", string(payload))) + l.svcCtx.DeviceManager.Broadcast(device.SubscribeUpdate) + return nil +} diff --git a/internal/logic/admin/server/updateRuleGroupLogic.go b/internal/logic/admin/server/updateRuleGroupLogic.go new file mode 100644 index 0000000..33193e1 --- /dev/null +++ b/internal/logic/admin/server/updateRuleGroupLogic.go @@ -0,0 +1,50 @@ +package server + +import ( + "context" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/tool" + + "github.com/perfect-panel/ppanel-server/internal/model/server" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type UpdateRuleGroupLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewUpdateRuleGroupLogic Update rule group +func NewUpdateRuleGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateRuleGroupLogic { + return &UpdateRuleGroupLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateRuleGroupLogic) UpdateRuleGroup(req *types.UpdateRuleGroupRequest) error { + rs, err := parseAndValidateRules(req.Rules, req.Name) + if err != nil { + return err + } + err = l.svcCtx.ServerModel.UpdateRuleGroup(l.ctx, &server.RuleGroup{ + Id: req.Id, + Icon: req.Icon, + Name: req.Name, + Tags: tool.StringSliceToString(req.Tags), + Rules: strings.Join(rs, "\n"), + Enable: req.Enable, + }) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), err.Error()) + } + return nil +} diff --git a/internal/logic/admin/subscribe/batchDeleteSubscribeGroupLogic.go b/internal/logic/admin/subscribe/batchDeleteSubscribeGroupLogic.go new file mode 100644 index 0000000..7bc9189 --- /dev/null +++ b/internal/logic/admin/subscribe/batchDeleteSubscribeGroupLogic.go @@ -0,0 +1,36 @@ +package subscribe + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type BatchDeleteSubscribeGroupLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Batch delete subscribe group +func NewBatchDeleteSubscribeGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BatchDeleteSubscribeGroupLogic { + return &BatchDeleteSubscribeGroupLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *BatchDeleteSubscribeGroupLogic) BatchDeleteSubscribeGroup(req *types.BatchDeleteSubscribeGroupRequest) error { + err := l.svcCtx.DB.Model(&subscribe.Group{}).Where("id in ?", req.Ids).Delete(&subscribe.Group{}).Error + if err != nil { + l.Logger.Error("[BatchDeleteSubscribeGroup] Delete Database Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "batch delete subscribe group failed: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/subscribe/batchDeleteSubscribeLogic.go b/internal/logic/admin/subscribe/batchDeleteSubscribeLogic.go new file mode 100644 index 0000000..eaee569 --- /dev/null +++ b/internal/logic/admin/subscribe/batchDeleteSubscribeLogic.go @@ -0,0 +1,59 @@ +package subscribe + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type BatchDeleteSubscribeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Batch delete subscribe +func NewBatchDeleteSubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BatchDeleteSubscribeLogic { + return &BatchDeleteSubscribeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +var errorIsExistActiveUser = errors.New("subscription ID belongs to an active user subscription") + +func (l *BatchDeleteSubscribeLogic) BatchDeleteSubscribe(req *types.BatchDeleteSubscribeRequest) error { + err := l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { + for _, id := range req.Ids { + var count int64 + // Validate whether the subscription ID belongs to an active user subscription. + if err := tx.Model(&user.Subscribe{}).Where("subscribe_id = ? AND status = 1", id).Count(&count).Find(&user.Subscribe{}).Error; err != nil { + l.Logger.Error("[BatchDeleteSubscribe] Query Subscribe Error: ", logger.Field("error", err.Error())) + return err + } + if count > 0 { + return errorIsExistActiveUser + } + if err := l.svcCtx.SubscribeModel.Delete(l.ctx, id, tx); err != nil { + return err + } + } + return nil + }) + if err != nil { + if errors.Is(err, errorIsExistActiveUser) { + return errors.Wrapf(xerr.NewErrCode(xerr.SubscribeIsUsedError), "subscription ID belongs to an active user subscription") + } + l.Logger.Error("[BatchDeleteSubscribe] Transaction Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete subscribe failed: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/subscribe/createSubscribeGroupLogic.go b/internal/logic/admin/subscribe/createSubscribeGroupLogic.go new file mode 100644 index 0000000..e21d2ea --- /dev/null +++ b/internal/logic/admin/subscribe/createSubscribeGroupLogic.go @@ -0,0 +1,39 @@ +package subscribe + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type CreateSubscribeGroupLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Create subscribe group +func NewCreateSubscribeGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateSubscribeGroupLogic { + return &CreateSubscribeGroupLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateSubscribeGroupLogic) CreateSubscribeGroup(req *types.CreateSubscribeGroupRequest) error { + err := l.svcCtx.DB.Model(&subscribe.Group{}).Create(&subscribe.Group{ + Name: req.Name, + Description: req.Description, + }).Error + if err != nil { + l.Logger.Error("[CreateSubscribeGroupLogic] create subscribe group failed: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create subscribe group failed: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/subscribe/createSubscribeLogic.go b/internal/logic/admin/subscribe/createSubscribeLogic.go new file mode 100644 index 0000000..1c8c912 --- /dev/null +++ b/internal/logic/admin/subscribe/createSubscribeLogic.go @@ -0,0 +1,68 @@ +package subscribe + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/ppanel-server/internal/model/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type CreateSubscribeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewCreateSubscribeLogic Create subscribe +func NewCreateSubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateSubscribeLogic { + return &CreateSubscribeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateSubscribeLogic) CreateSubscribe(req *types.CreateSubscribeRequest) error { + discount := "" + if len(req.Discount) > 0 { + val, _ := json.Marshal(req.Discount) + discount = string(val) + } + sub := &subscribe.Subscribe{ + Id: 0, + Name: req.Name, + Description: req.Description, + UnitPrice: req.UnitPrice, + UnitTime: req.UnitTime, + Discount: discount, + Replacement: req.Replacement, + Inventory: req.Inventory, + Traffic: req.Traffic, + SpeedLimit: req.SpeedLimit, + DeviceLimit: req.DeviceLimit, + Quota: req.Quota, + GroupId: req.GroupId, + ServerGroup: tool.Int64SliceToString(req.ServerGroup), + Server: tool.Int64SliceToString(req.Server), + Show: req.Show, + Sell: req.Sell, + Sort: 0, + DeductionRatio: req.DeductionRatio, + AllowDeduction: req.AllowDeduction, + ResetCycle: req.ResetCycle, + RenewalReset: req.RenewalReset, + } + err := l.svcCtx.SubscribeModel.Insert(l.ctx, sub) + if err != nil { + l.Logger.Error("[CreateSubscribeLogic] create subscribe error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create subscribe error: %v", err.Error()) + } + + return nil +} diff --git a/internal/logic/admin/subscribe/deleteSubscribeGroupLogic.go b/internal/logic/admin/subscribe/deleteSubscribeGroupLogic.go new file mode 100644 index 0000000..655febc --- /dev/null +++ b/internal/logic/admin/subscribe/deleteSubscribeGroupLogic.go @@ -0,0 +1,36 @@ +package subscribe + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type DeleteSubscribeGroupLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Delete subscribe group +func NewDeleteSubscribeGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteSubscribeGroupLogic { + return &DeleteSubscribeGroupLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteSubscribeGroupLogic) DeleteSubscribeGroup(req *types.DeleteSubscribeGroupRequest) error { + err := l.svcCtx.DB.Model(&subscribe.Group{}).Where("id = ?", req.Id).Delete(&subscribe.Group{}).Error + if err != nil { + l.Logger.Error("[DeleteSubscribeGroupLogic] delete subscribe group failed: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete subscribe group failed: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/subscribe/deleteSubscribeLogic.go b/internal/logic/admin/subscribe/deleteSubscribeLogic.go new file mode 100644 index 0000000..44d22fc --- /dev/null +++ b/internal/logic/admin/subscribe/deleteSubscribeLogic.go @@ -0,0 +1,51 @@ +package subscribe + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type DeleteSubscribeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Delete subscribe +func NewDeleteSubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteSubscribeLogic { + return &DeleteSubscribeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteSubscribeLogic) DeleteSubscribe(req *types.DeleteSubscribeRequest) error { + // Check if the subscribe exists + var total int64 + err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + return db.Model(&user.Subscribe{}).Where("subscribe_id = ? AND `status` = ?", req.Id, 1).Count(&total).Find(&user.Subscribe{}).Error + }) + if err != nil { + l.Logger.Error("[DeleteSubscribeLogic] check subscribe failed: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "check subscribe failed: %v", err.Error()) + } + if total != 0 { + return errors.Wrapf(xerr.NewErrCode(xerr.SubscribeIsUsedError), "subscribe is used") + } + + err = l.svcCtx.SubscribeModel.Delete(l.ctx, req.Id) + if err != nil { + l.Logger.Error("[DeleteSubscribeLogic] delete subscribe failed: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete subscribe failed: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/subscribe/getSubscribeDetailsLogic.go b/internal/logic/admin/subscribe/getSubscribeDetailsLogic.go new file mode 100644 index 0000000..26f4be4 --- /dev/null +++ b/internal/logic/admin/subscribe/getSubscribeDetailsLogic.go @@ -0,0 +1,47 @@ +package subscribe + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetSubscribeDetailsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get subscribe details +func NewGetSubscribeDetailsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSubscribeDetailsLogic { + return &GetSubscribeDetailsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetSubscribeDetailsLogic) GetSubscribeDetails(req *types.GetSubscribeDetailsRequest) (resp *types.Subscribe, err error) { + sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.Id) + if err != nil { + l.Logger.Error("[GetSubscribeDetailsLogic] get subscribe details failed: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get subscribe details failed: %v", err.Error()) + } + resp = &types.Subscribe{} + tool.DeepCopy(resp, sub) + if sub.Discount != "" { + err = json.Unmarshal([]byte(sub.Discount), &resp.Discount) + if err != nil { + l.Logger.Error("[GetSubscribeDetailsLogic] JSON unmarshal failed: ", logger.Field("error", err.Error()), logger.Field("discount", sub.Discount)) + } + } + resp.Server = tool.StringToInt64Slice(sub.Server) + resp.ServerGroup = tool.StringToInt64Slice(sub.ServerGroup) + return resp, nil +} diff --git a/internal/logic/admin/subscribe/getSubscribeGroupListLogic.go b/internal/logic/admin/subscribe/getSubscribeGroupListLogic.go new file mode 100644 index 0000000..81c65b7 --- /dev/null +++ b/internal/logic/admin/subscribe/getSubscribeGroupListLogic.go @@ -0,0 +1,44 @@ +package subscribe + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetSubscribeGroupListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get subscribe group list +func NewGetSubscribeGroupListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSubscribeGroupListLogic { + return &GetSubscribeGroupListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetSubscribeGroupListLogic) GetSubscribeGroupList() (resp *types.GetSubscribeGroupListResponse, err error) { + var list []*subscribe.Group + var total int64 + err = l.svcCtx.DB.Model(&subscribe.Group{}).Count(&total).Find(&list).Error + if err != nil { + l.Logger.Error("[GetSubscribeGroupListLogic] get subscribe group list failed: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get subscribe group list failed: %v", err.Error()) + } + groupList := make([]types.SubscribeGroup, 0) + tool.DeepCopy(&groupList, list) + return &types.GetSubscribeGroupListResponse{ + Total: total, + List: groupList, + }, nil +} diff --git a/internal/logic/admin/subscribe/getSubscribeListLogic.go b/internal/logic/admin/subscribe/getSubscribeListLogic.go new file mode 100644 index 0000000..20d5455 --- /dev/null +++ b/internal/logic/admin/subscribe/getSubscribeListLogic.go @@ -0,0 +1,72 @@ +package subscribe + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetSubscribeListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get subscribe list +func NewGetSubscribeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSubscribeListLogic { + return &GetSubscribeListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetSubscribeListLogic) GetSubscribeList(req *types.GetSubscribeListRequest) (resp *types.GetSubscribeListResponse, err error) { + total, list, err := l.svcCtx.SubscribeModel.QuerySubscribeListByPage(l.ctx, int(req.Page), int(req.Size), req.GroupId, req.Search) + if err != nil { + l.Logger.Error("[GetSubscribeListLogic] get subscribe list failed: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get subscribe list failed: %v", err.Error()) + } + var ( + subscribeIdList = make([]int64, 0, len(list)) + resultList = make([]types.SubscribeItem, 0, len(list)) + ) + for _, item := range list { + subscribeIdList = append(subscribeIdList, item.Id) + var sub types.SubscribeItem + tool.DeepCopy(&sub, item) + if item.Discount != "" { + err = json.Unmarshal([]byte(item.Discount), &sub.Discount) + if err != nil { + l.Logger.Error("[GetSubscribeListLogic] JSON unmarshal failed: ", logger.Field("error", err.Error()), logger.Field("discount", item.Discount)) + } + } + sub.Server = tool.StringToInt64Slice(item.Server) + sub.ServerGroup = tool.StringToInt64Slice(item.ServerGroup) + resultList = append(resultList, sub) + } + + subscribeMaps, err := l.svcCtx.UserModel.QueryActiveSubscriptions(l.ctx, subscribeIdList...) + if err != nil { + l.Logger.Error("[GetSubscribeListLogic] get user subscribe failed: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get user subscribe failed: %v", err.Error()) + } + + for i, item := range resultList { + if subscribe, ok := subscribeMaps[item.Id]; ok { + resultList[i].Sold = subscribe + } + } + + resp = &types.GetSubscribeListResponse{ + Total: total, + List: resultList, + } + return +} diff --git a/internal/logic/admin/subscribe/subscribeSortLogic.go b/internal/logic/admin/subscribe/subscribeSortLogic.go new file mode 100644 index 0000000..f4d7b08 --- /dev/null +++ b/internal/logic/admin/subscribe/subscribeSortLogic.go @@ -0,0 +1,64 @@ +package subscribe + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type SubscribeSortLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewSubscribeSortLogic Subscribe sort +func NewSubscribeSortLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SubscribeSortLogic { + return &SubscribeSortLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SubscribeSortLogic) SubscribeSort(req *types.SubscribeSortRequest) error { + var sort = make(map[int64]int64, len(req.Sort)) + var ids []int64 + for i, v := range req.Sort { + sort[v.Id] = int64(i) + ids = append(ids, v.Id) + } + // query min sort by ids + minSort, err := l.svcCtx.SubscribeModel.QuerySubscribeMinSortByIds(l.ctx, ids) + if err != nil { + l.Logger.Error("[SubscribeSortLogic] query subscribe list by ids error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query subscribe list by ids error: %v", err.Error()) + } + subs, err := l.svcCtx.SubscribeModel.QuerySubscribeListByIds(l.ctx, ids) + if err != nil { + l.Logger.Error("[SubscribeSortLogic] query subscribe list by ids error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query subscribe list by ids error: %v", err.Error()) + } + // reordering + for _, sub := range subs { + if newSort, ok := sort[sub.Id]; ok { + sub.Sort = minSort + newSort + } + } + // update sort + err = l.svcCtx.SubscribeModel.Transaction(l.ctx, func(db *gorm.DB) error { + return db.Save(subs).Error + }) + if err != nil { + l.Logger.Error("[SubscribeSortLogic] update subscribe sort error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update subscribe sort error: %v", err.Error()) + } + l.Logger.Info("[UpdateSubscribeSort] Successfully updated subscribe sort") + return nil +} diff --git a/internal/logic/admin/subscribe/updateSubscribeGroupLogic.go b/internal/logic/admin/subscribe/updateSubscribeGroupLogic.go new file mode 100644 index 0000000..5eb4119 --- /dev/null +++ b/internal/logic/admin/subscribe/updateSubscribeGroupLogic.go @@ -0,0 +1,40 @@ +package subscribe + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateSubscribeGroupLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Update subscribe group +func NewUpdateSubscribeGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateSubscribeGroupLogic { + return &UpdateSubscribeGroupLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateSubscribeGroupLogic) UpdateSubscribeGroup(req *types.UpdateSubscribeGroupRequest) error { + err := l.svcCtx.DB.Model(&subscribe.Group{}).Where("id = ?", req.Id).Save(&subscribe.Group{ + Id: req.Id, + Name: req.Name, + Description: req.Description, + }).Error + if err != nil { + l.Logger.Error("[UpdateSubscribeGroup] update subscribe group failed", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update subscribe group failed: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/subscribe/updateSubscribeLogic.go b/internal/logic/admin/subscribe/updateSubscribeLogic.go new file mode 100644 index 0000000..e61df40 --- /dev/null +++ b/internal/logic/admin/subscribe/updateSubscribeLogic.go @@ -0,0 +1,76 @@ +package subscribe + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/ppanel-server/pkg/device" + + "github.com/perfect-panel/ppanel-server/internal/model/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateSubscribeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Update subscribe +func NewUpdateSubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateSubscribeLogic { + return &UpdateSubscribeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateSubscribeLogic) UpdateSubscribe(req *types.UpdateSubscribeRequest) error { + // Query the database to get the subscribe information + _, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.Id) + if err != nil { + l.Logger.Error("[UpdateSubscribe] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", req.Id)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get subscribe error: %v", err.Error()) + } + discount := "" + if len(req.Discount) > 0 { + val, _ := json.Marshal(req.Discount) + discount = string(val) + } + sub := &subscribe.Subscribe{ + Id: req.Id, + Name: req.Name, + Description: req.Description, + UnitPrice: req.UnitPrice, + UnitTime: req.UnitTime, + Discount: discount, + Replacement: req.Replacement, + Inventory: req.Inventory, + Traffic: req.Traffic, + SpeedLimit: req.SpeedLimit, + DeviceLimit: req.DeviceLimit, + Quota: req.Quota, + GroupId: req.GroupId, + ServerGroup: tool.Int64SliceToString(req.ServerGroup), + Server: tool.Int64SliceToString(req.Server), + Show: req.Show, + Sell: req.Sell, + Sort: req.Sort, + DeductionRatio: req.DeductionRatio, + AllowDeduction: req.AllowDeduction, + ResetCycle: req.ResetCycle, + RenewalReset: req.RenewalReset, + } + err = l.svcCtx.SubscribeModel.Update(l.ctx, sub) + if err != nil { + l.Logger.Error("[UpdateSubscribe] update subscribe failed", logger.Field("error", err.Error()), logger.Field("subscribe", sub)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update subscribe error: %v", err.Error()) + } + l.svcCtx.DeviceManager.Broadcast(device.SubscribeUpdate) + return nil +} diff --git a/internal/logic/admin/system/createApplicationLogic.go b/internal/logic/admin/system/createApplicationLogic.go new file mode 100644 index 0000000..89aeecb --- /dev/null +++ b/internal/logic/admin/system/createApplicationLogic.go @@ -0,0 +1,125 @@ +package system + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/application" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type CreateApplicationLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewCreateApplicationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateApplicationLogic { + return &CreateApplicationLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateApplicationLogic) CreateApplication(req *types.CreateApplicationRequest) error { + var ios []application.ApplicationVersion + if len(req.Platform.IOS) > 0 { + for _, ios_ := range req.Platform.IOS { + ios = append(ios, application.ApplicationVersion{ + Url: ios_.Url, + Version: ios_.Version, + Platform: "ios", + IsDefault: ios_.IsDefault, + Description: ios_.Description, + }) + } + } + + var mac []application.ApplicationVersion + if len(req.Platform.MacOS) > 0 { + for _, mac_ := range req.Platform.MacOS { + mac = append(mac, application.ApplicationVersion{ + Url: mac_.Url, + Version: mac_.Version, + Platform: "macos", + IsDefault: mac_.IsDefault, + Description: mac_.Description, + }) + } + } + + var linux []application.ApplicationVersion + if len(req.Platform.Linux) > 0 { + for _, linux_ := range req.Platform.Linux { + linux = append(linux, application.ApplicationVersion{ + Url: linux_.Url, + Version: linux_.Version, + Platform: "linux", + IsDefault: linux_.IsDefault, + Description: linux_.Description, + }) + } + } + + var android []application.ApplicationVersion + if len(req.Platform.Android) > 0 { + for _, android_ := range req.Platform.Android { + android = append(android, application.ApplicationVersion{ + Url: android_.Url, + Version: android_.Version, + Platform: "android", + IsDefault: android_.IsDefault, + Description: android_.Description, + }) + } + } + + var windows []application.ApplicationVersion + if len(req.Platform.Windows) > 0 { + for _, windows_ := range req.Platform.Windows { + windows = append(windows, application.ApplicationVersion{ + Url: windows_.Url, + Version: windows_.Version, + Platform: "windows", + IsDefault: windows_.IsDefault, + Description: windows_.Description, + }) + } + } + + var harmony []application.ApplicationVersion + if len(req.Platform.Harmony) > 0 { + for _, harmony_ := range req.Platform.Harmony { + harmony = append(harmony, application.ApplicationVersion{ + Url: harmony_.Url, + Version: harmony_.Version, + Platform: "harmony", + IsDefault: harmony_.IsDefault, + Description: harmony_.Description, + }) + } + } + var applicationVersions []application.ApplicationVersion + applicationVersions = append(applicationVersions, ios...) + applicationVersions = append(applicationVersions, mac...) + applicationVersions = append(applicationVersions, linux...) + applicationVersions = append(applicationVersions, android...) + applicationVersions = append(applicationVersions, windows...) + applicationVersions = append(applicationVersions, harmony...) + app := application.Application{ + Name: req.Name, + Icon: req.Icon, + SubscribeType: req.SubscribeType, + ApplicationVersions: applicationVersions, + } + err := l.svcCtx.ApplicationModel.Insert(l.ctx, &app) + if err != nil { + l.Errorw("[CreateApplicationLogic] create application error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create application error: %v", err) + } + return nil +} diff --git a/internal/logic/admin/system/createApplicationVersionLogic.go b/internal/logic/admin/system/createApplicationVersionLogic.go new file mode 100644 index 0000000..7ee2033 --- /dev/null +++ b/internal/logic/admin/system/createApplicationVersionLogic.go @@ -0,0 +1,44 @@ +package system + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/application" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type CreateApplicationVersionLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Create application version +func NewCreateApplicationVersionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateApplicationVersionLogic { + return &CreateApplicationVersionLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateApplicationVersionLogic) CreateApplicationVersion(req *types.CreateApplicationVersionRequest) error { + create := &application.ApplicationVersion{ + Url: req.Url, + Platform: req.Platform, + Version: req.Version, + Description: req.Description, + IsDefault: req.IsDefault, + ApplicationId: req.ApplicationId, + } + err := l.svcCtx.ApplicationModel.InsertVersion(l.ctx, create) + if err != nil { + l.Errorw("[CreateApplicationVersion] create application version error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create application version error: %v", err) + } + return nil +} diff --git a/internal/logic/admin/system/deleteApplicationLogic.go b/internal/logic/admin/system/deleteApplicationLogic.go new file mode 100644 index 0000000..4e7e489 --- /dev/null +++ b/internal/logic/admin/system/deleteApplicationLogic.go @@ -0,0 +1,35 @@ +package system + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type DeleteApplicationLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewDeleteApplicationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteApplicationLogic { + return &DeleteApplicationLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteApplicationLogic) DeleteApplication(req *types.DeleteApplicationRequest) error { + // delete application + err := l.svcCtx.ApplicationModel.Delete(l.ctx, req.Id) + if err != nil { + l.Errorw("[DeleteApplicationLogic] delete application error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete application error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/system/deleteApplicationVersionLogic.go b/internal/logic/admin/system/deleteApplicationVersionLogic.go new file mode 100644 index 0000000..f2235e5 --- /dev/null +++ b/internal/logic/admin/system/deleteApplicationVersionLogic.go @@ -0,0 +1,36 @@ +package system + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type DeleteApplicationVersionLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Delete application +func NewDeleteApplicationVersionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteApplicationVersionLogic { + return &DeleteApplicationVersionLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteApplicationVersionLogic) DeleteApplicationVersion(req *types.DeleteApplicationVersionRequest) error { + // delete application + err := l.svcCtx.ApplicationModel.DeleteVersion(l.ctx, req.Id) + if err != nil { + l.Errorw("[DeleteApplicationVersion] delete application version error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete application version error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/system/getApplicationConfigLogic.go b/internal/logic/admin/system/getApplicationConfigLogic.go new file mode 100644 index 0000000..39a853f --- /dev/null +++ b/internal/logic/admin/system/getApplicationConfigLogic.go @@ -0,0 +1,49 @@ +package system + +import ( + "context" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type GetApplicationConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// get application config +func NewGetApplicationConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetApplicationConfigLogic { + return &GetApplicationConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetApplicationConfigLogic) GetApplicationConfig() (resp *types.ApplicationConfig, err error) { + resp = &types.ApplicationConfig{} + appConfig, err := l.svcCtx.ApplicationModel.FindOneConfig(l.ctx, 1) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + err = nil + return + } + l.Errorw("[GetApplicationConfig] Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get app config error: %v", err.Error()) + } + resp.AppId = appConfig.AppId + resp.EncryptionKey = appConfig.EncryptionKey + resp.EncryptionMethod = appConfig.EncryptionMethod + resp.Domains = strings.Split(appConfig.Domains, ";") + resp.StartupPicture = appConfig.StartupPicture + resp.StartupPictureSkipTime = appConfig.StartupPictureSkipTime + return +} diff --git a/internal/logic/admin/system/getApplicationLogic.go b/internal/logic/admin/system/getApplicationLogic.go new file mode 100644 index 0000000..7c94d5c --- /dev/null +++ b/internal/logic/admin/system/getApplicationLogic.go @@ -0,0 +1,113 @@ +package system + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/application" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type GetApplicationLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get application +func NewGetApplicationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetApplicationLogic { + return &GetApplicationLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetApplicationLogic) GetApplication() (resp *types.ApplicationResponse, err error) { + resp = &types.ApplicationResponse{} + var applications []*application.Application + err = l.svcCtx.ApplicationModel.Transaction(l.ctx, func(tx *gorm.DB) (err error) { + return tx.Model(applications).Preload("ApplicationVersions").Find(&applications).Error + }) + if err != nil { + l.Errorw("[GetApplicationLogic] get application error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get application error: %v", err.Error()) + } + + if len(applications) == 0 { + return resp, nil + } + + for _, app := range applications { + applicationResponse := types.ApplicationResponseInfo{ + Id: app.Id, + Name: app.Name, + Icon: app.Icon, + Description: app.Description, + SubscribeType: app.SubscribeType, + } + applicationVersions := app.ApplicationVersions + if len(applicationVersions) != 0 { + for _, applicationVersion := range applicationVersions { + switch applicationVersion.Platform { + case "ios": + applicationResponse.Platform.IOS = append(applicationResponse.Platform.IOS, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + case "macos": + applicationResponse.Platform.MacOS = append(applicationResponse.Platform.MacOS, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + case "linux": + applicationResponse.Platform.Linux = append(applicationResponse.Platform.Linux, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + case "android": + applicationResponse.Platform.Android = append(applicationResponse.Platform.Android, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + case "windows": + applicationResponse.Platform.Windows = append(applicationResponse.Platform.Windows, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + case "harmony": + applicationResponse.Platform.Harmony = append(applicationResponse.Platform.Harmony, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + } + } + } + resp.Applications = append(resp.Applications, applicationResponse) + } + + return +} diff --git a/internal/logic/admin/system/getCurrencyConfigLogic.go b/internal/logic/admin/system/getCurrencyConfigLogic.go new file mode 100644 index 0000000..42286b3 --- /dev/null +++ b/internal/logic/admin/system/getCurrencyConfigLogic.go @@ -0,0 +1,38 @@ +package system + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetCurrencyConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Currency Config +func NewGetCurrencyConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetCurrencyConfigLogic { + return &GetCurrencyConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetCurrencyConfigLogic) GetCurrencyConfig() (resp *types.CurrencyConfig, err error) { + configs, err := l.svcCtx.SystemModel.GetCurrencyConfig(l.ctx) + if err != nil { + l.Errorw("[GetCurrencyConfigLogic] GetCurrencyConfig error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetCurrencyConfig error: %v", err.Error()) + } + resp = &types.CurrencyConfig{} + tool.SystemConfigSliceReflectToStruct(configs, resp) + return +} diff --git a/internal/logic/admin/system/getInviteConfigLogic.go b/internal/logic/admin/system/getInviteConfigLogic.go new file mode 100644 index 0000000..b12ac8f --- /dev/null +++ b/internal/logic/admin/system/getInviteConfigLogic.go @@ -0,0 +1,40 @@ +package system + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetInviteConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetInviteConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetInviteConfigLogic { + return &GetInviteConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetInviteConfigLogic) GetInviteConfig() (*types.InviteConfig, error) { + resp := &types.InviteConfig{} + // get invite config from db + configs, err := l.svcCtx.SystemModel.GetInviteConfig(l.ctx) + if err != nil { + l.Errorw("[GetInviteConfigLogic] get invite config error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get invite config error: %v", err.Error()) + } + // reflect to response + tool.SystemConfigSliceReflectToStruct(configs, resp) + + return resp, nil +} diff --git a/internal/logic/admin/system/getNodeConfigLogic.go b/internal/logic/admin/system/getNodeConfigLogic.go new file mode 100644 index 0000000..caa647a --- /dev/null +++ b/internal/logic/admin/system/getNodeConfigLogic.go @@ -0,0 +1,40 @@ +package system + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetNodeConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetNodeConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetNodeConfigLogic { + return &GetNodeConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetNodeConfigLogic) GetNodeConfig() (*types.NodeConfig, error) { + resp := &types.NodeConfig{} + + // get server config from db + configs, err := l.svcCtx.SystemModel.GetNodeConfig(l.ctx) + if err != nil { + l.Errorw("[GetNodeConfigLogic] GetNodeConfig get server config error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetNodeConfig get server config error: %v", err.Error()) + } + // reflect to response + tool.SystemConfigSliceReflectToStruct(configs, resp) + return resp, nil +} diff --git a/internal/logic/admin/system/getNodeMultiplierLogic.go b/internal/logic/admin/system/getNodeMultiplierLogic.go new file mode 100644 index 0000000..cc330e4 --- /dev/null +++ b/internal/logic/admin/system/getNodeMultiplierLogic.go @@ -0,0 +1,46 @@ +package system + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetNodeMultiplierLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Node Multiplier +func NewGetNodeMultiplierLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetNodeMultiplierLogic { + return &GetNodeMultiplierLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetNodeMultiplierLogic) GetNodeMultiplier() (resp *types.GetNodeMultiplierResponse, err error) { + data, err := l.svcCtx.SystemModel.FindNodeMultiplierConfig(l.ctx) + if err != nil { + l.Logger.Error("Get Node Multiplier Config Error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Get Node Multiplier Config Error: %s", err.Error()) + } + var periods []types.TimePeriod + if data.Value != "" { + if err := json.Unmarshal([]byte(data.Value), &periods); err != nil { + l.Logger.Error("Unmarshal Node Multiplier Config Error: ", logger.Field("error", err.Error()), logger.Field("value", data.Value)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal Node Multiplier Config Error: %s", err.Error()) + } + } + + return &types.GetNodeMultiplierResponse{ + Periods: periods, + }, nil +} diff --git a/internal/logic/admin/system/getPrivacyPolicyConfigLogic.go b/internal/logic/admin/system/getPrivacyPolicyConfigLogic.go new file mode 100644 index 0000000..8c8d15d --- /dev/null +++ b/internal/logic/admin/system/getPrivacyPolicyConfigLogic.go @@ -0,0 +1,40 @@ +package system + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetPrivacyPolicyConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewGetPrivacyPolicyConfigLogic get Privacy Policy Config +func NewGetPrivacyPolicyConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPrivacyPolicyConfigLogic { + return &GetPrivacyPolicyConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetPrivacyPolicyConfigLogic) GetPrivacyPolicyConfig() (resp *types.PrivacyPolicyConfig, err error) { + resp = &types.PrivacyPolicyConfig{} + // get tos config from db + configs, err := l.svcCtx.SystemModel.GetTosConfig(l.ctx) + if err != nil { + l.Errorw("[GetTosConfig] GetTosConfig error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetTosConfig error: %v", err.Error()) + } + // reflect to response + tool.SystemConfigSliceReflectToStruct(configs, resp) + return +} diff --git a/internal/logic/admin/system/getRegisterConfigLogic.go b/internal/logic/admin/system/getRegisterConfigLogic.go new file mode 100644 index 0000000..7fcdf3a --- /dev/null +++ b/internal/logic/admin/system/getRegisterConfigLogic.go @@ -0,0 +1,41 @@ +package system + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetRegisterConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetRegisterConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetRegisterConfigLogic { + return &GetRegisterConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetRegisterConfigLogic) GetRegisterConfig() (*types.RegisterConfig, error) { + resp := &types.RegisterConfig{} + + // get register config from database + configs, err := l.svcCtx.SystemModel.GetRegisterConfig(l.ctx) + if err != nil { + l.Errorw("[GetRegisterConfig] Database query error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get register config error: %v", err.Error()) + } + + // reflect to response + tool.SystemConfigSliceReflectToStruct(configs, resp) + return resp, nil +} diff --git a/internal/logic/admin/system/getSiteConfigLogic.go b/internal/logic/admin/system/getSiteConfigLogic.go new file mode 100644 index 0000000..ec7890d --- /dev/null +++ b/internal/logic/admin/system/getSiteConfigLogic.go @@ -0,0 +1,39 @@ +package system + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetSiteConfigLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logger.Logger +} + +func NewGetSiteConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSiteConfigLogic { + return &GetSiteConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetSiteConfigLogic) GetSiteConfig() (resp *types.SiteConfig, err error) { + resp = &types.SiteConfig{} + // get site config from db + siteConfigs, err := l.svcCtx.SystemModel.GetSiteConfig(l.ctx) + if err != nil { + l.Logger.Error("[GetSiteConfig] Database query error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get site config failed: %v", err.Error()) + } + // reflect to response + tool.SystemConfigSliceReflectToStruct(siteConfigs, resp) + return resp, nil +} diff --git a/internal/logic/admin/system/getSubscribeConfigLogic.go b/internal/logic/admin/system/getSubscribeConfigLogic.go new file mode 100644 index 0000000..4e36c81 --- /dev/null +++ b/internal/logic/admin/system/getSubscribeConfigLogic.go @@ -0,0 +1,40 @@ +package system + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetSubscribeConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetSubscribeConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSubscribeConfigLogic { + return &GetSubscribeConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetSubscribeConfigLogic) GetSubscribeConfig() (resp *types.SubscribeConfig, err error) { + resp = &types.SubscribeConfig{} + // get subscribe config from db + subscribeConfigs, err := l.svcCtx.SystemModel.GetSubscribeConfig(l.ctx) + if err != nil { + l.Errorw("[GetSubscribeConfig] Database query error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get subscribe config failed: %v", err.Error()) + } + + // reflect to response + tool.SystemConfigSliceReflectToStruct(subscribeConfigs, resp) + return resp, nil +} diff --git a/internal/logic/admin/system/getSubscribeTypeLogic.go b/internal/logic/admin/system/getSubscribeTypeLogic.go new file mode 100644 index 0000000..41ab1f9 --- /dev/null +++ b/internal/logic/admin/system/getSubscribeTypeLogic.go @@ -0,0 +1,42 @@ +package system + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/subscribeType" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetSubscribeTypeLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logger.Logger +} + +func NewGetSubscribeTypeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSubscribeTypeLogic { + return &GetSubscribeTypeLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logger.WithContext(ctx), + } +} + +func (l *GetSubscribeTypeLogic) GetSubscribeType() (resp *types.SubscribeType, err error) { + var list []*subscribeType.SubscribeType + err = l.svcCtx.DB.Model(&subscribeType.SubscribeType{}).Find(&list).Error + if err != nil { + l.Errorw("[GetSubscribeType] get subscribe type failed", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get subscribe type failed: %v", err) + } + typeList := make([]string, 0) + for _, item := range list { + typeList = append(typeList, item.Name) + } + return &types.SubscribeType{ + SubscribeTypes: typeList, + }, nil +} diff --git a/internal/logic/admin/system/getTosConfigLogic.go b/internal/logic/admin/system/getTosConfigLogic.go new file mode 100644 index 0000000..be2e9db --- /dev/null +++ b/internal/logic/admin/system/getTosConfigLogic.go @@ -0,0 +1,39 @@ +package system + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetTosConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetTosConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetTosConfigLogic { + return &GetTosConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetTosConfigLogic) GetTosConfig() (resp *types.TosConfig, err error) { + resp = &types.TosConfig{} + // get tos config from db + configs, err := l.svcCtx.SystemModel.GetTosConfig(l.ctx) + if err != nil { + l.Errorw("[GetTosConfig] GetTosConfig error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetTosConfig error: %v", err.Error()) + } + // reflect to response + tool.SystemConfigSliceReflectToStruct(configs, resp) + return +} diff --git a/internal/logic/admin/system/getVerifyCodeConfigLogic.go b/internal/logic/admin/system/getVerifyCodeConfigLogic.go new file mode 100644 index 0000000..52db722 --- /dev/null +++ b/internal/logic/admin/system/getVerifyCodeConfigLogic.go @@ -0,0 +1,38 @@ +package system + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetVerifyCodeConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Verify Code Config +func NewGetVerifyCodeConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetVerifyCodeConfigLogic { + return &GetVerifyCodeConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetVerifyCodeConfigLogic) GetVerifyCodeConfig() (resp *types.VerifyCodeConfig, err error) { + data, err := l.svcCtx.SystemModel.GetVerifyCodeConfig(l.ctx) + if err != nil { + l.Errorw("Get Verify Code Config Error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Get Verify Code Config Error: %s", err.Error()) + } + resp = &types.VerifyCodeConfig{} + tool.SystemConfigSliceReflectToStruct(data, resp) + return +} diff --git a/internal/logic/admin/system/getVerifyConfigLogic.go b/internal/logic/admin/system/getVerifyConfigLogic.go new file mode 100644 index 0000000..5568430 --- /dev/null +++ b/internal/logic/admin/system/getVerifyConfigLogic.go @@ -0,0 +1,41 @@ +package system + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/initialize" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetVerifyConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetVerifyConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetVerifyConfigLogic { + return &GetVerifyConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetVerifyConfigLogic) GetVerifyConfig() (*types.VerifyConfig, error) { + resp := &types.VerifyConfig{} + // get verify config from db + verifyConfigs, err := l.svcCtx.SystemModel.GetVerifyConfig(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get verify config failed: %v", err.Error()) + } + // reflect to response + tool.SystemConfigSliceReflectToStruct(verifyConfigs, resp) + // update verify config to system + initialize.Verify(l.svcCtx) + return resp, nil +} diff --git a/internal/logic/admin/system/setNodeMultiplierLogic.go b/internal/logic/admin/system/setNodeMultiplierLogic.go new file mode 100644 index 0000000..4f4b6ae --- /dev/null +++ b/internal/logic/admin/system/setNodeMultiplierLogic.go @@ -0,0 +1,40 @@ +package system + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type SetNodeMultiplierLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Set Node Multiplier +func NewSetNodeMultiplierLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SetNodeMultiplierLogic { + return &SetNodeMultiplierLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SetNodeMultiplierLogic) SetNodeMultiplier(req *types.SetNodeMultiplierRequest) error { + data, err := json.Marshal(req.Periods) + if err != nil { + l.Logger.Error("Marshal Node Multiplier Config Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Marshal Node Multiplier Config Error: %s", err.Error()) + } + if err := l.svcCtx.SystemModel.UpdateNodeMultiplierConfig(l.ctx, string(data)); err != nil { + l.Logger.Error("Update Node Multiplier Config Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Update Node Multiplier Config Error: %s", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/system/settingTelegramBotLogic.go b/internal/logic/admin/system/settingTelegramBotLogic.go new file mode 100644 index 0000000..c919743 --- /dev/null +++ b/internal/logic/admin/system/settingTelegramBotLogic.go @@ -0,0 +1,30 @@ +package system + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/initialize" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type SettingTelegramBotLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewSettingTelegramBotLogic setting telegram bot +func NewSettingTelegramBotLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SettingTelegramBotLogic { + return &SettingTelegramBotLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SettingTelegramBotLogic) SettingTelegramBot() error { + initialize.Telegram(l.svcCtx) + return nil +} diff --git a/internal/logic/admin/system/updateApplicationConfigLogic.go b/internal/logic/admin/system/updateApplicationConfigLogic.go new file mode 100644 index 0000000..2b5c936 --- /dev/null +++ b/internal/logic/admin/system/updateApplicationConfigLogic.go @@ -0,0 +1,45 @@ +package system + +import ( + "context" + "strings" + + "github.com/perfect-panel/ppanel-server/internal/model/application" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateApplicationConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// update application config +func NewUpdateApplicationConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateApplicationConfigLogic { + return &UpdateApplicationConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateApplicationConfigLogic) UpdateApplicationConfig(req *types.ApplicationConfig) error { + err := l.svcCtx.ApplicationModel.UpdateConfig(l.ctx, &application.ApplicationConfig{ + Id: 1, + AppId: req.AppId, + EncryptionKey: req.EncryptionKey, + EncryptionMethod: req.EncryptionMethod, + Domains: strings.Join(req.Domains, ";"), + StartupPicture: req.StartupPicture, + StartupPictureSkipTime: req.StartupPictureSkipTime, + }) + if err != nil { + l.Errorw("[UpdateApplicationConfig] Database Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update app config error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/system/updateApplicationLogic.go b/internal/logic/admin/system/updateApplicationLogic.go new file mode 100644 index 0000000..dd12e22 --- /dev/null +++ b/internal/logic/admin/system/updateApplicationLogic.go @@ -0,0 +1,149 @@ +package system + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/application" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" +) + +type UpdateApplicationLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUpdateApplicationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateApplicationLogic { + return &UpdateApplicationLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateApplicationLogic) UpdateApplication(req *types.UpdateApplicationRequest) error { + + // find application + app, err := l.svcCtx.ApplicationModel.FindOne(l.ctx, req.Id) + if err != nil { + l.Errorw("[UpdateApplication] find application error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find application error: %v", err.Error()) + } + app.Name = req.Name + app.Icon = req.Icon + app.SubscribeType = req.SubscribeType + app.Description = req.Description + + var ios []application.ApplicationVersion + if len(req.Platform.IOS) > 0 { + for _, ios_ := range req.Platform.IOS { + ios = append(ios, application.ApplicationVersion{ + Url: ios_.Url, + Version: ios_.Version, + Platform: "ios", + IsDefault: ios_.IsDefault, + Description: ios_.Description, + ApplicationId: app.Id, + }) + } + } + + var mac []application.ApplicationVersion + if len(req.Platform.MacOS) > 0 { + for _, mac_ := range req.Platform.MacOS { + mac = append(mac, application.ApplicationVersion{ + Url: mac_.Url, + Version: mac_.Version, + Platform: "macos", + IsDefault: mac_.IsDefault, + Description: mac_.Description, + ApplicationId: app.Id, + }) + } + } + + var linux []application.ApplicationVersion + if len(req.Platform.Linux) > 0 { + for _, linux_ := range req.Platform.Linux { + linux = append(linux, application.ApplicationVersion{ + Url: linux_.Url, + Version: linux_.Version, + Platform: "linux", + IsDefault: linux_.IsDefault, + Description: linux_.Description, + ApplicationId: app.Id, + }) + } + } + + var android []application.ApplicationVersion + if len(req.Platform.Android) > 0 { + for _, android_ := range req.Platform.Android { + android = append(android, application.ApplicationVersion{ + Url: android_.Url, + Version: android_.Version, + Platform: "android", + IsDefault: android_.IsDefault, + Description: android_.Description, + ApplicationId: app.Id, + }) + } + } + + var windows []application.ApplicationVersion + if len(req.Platform.Windows) > 0 { + for _, windows_ := range req.Platform.Windows { + windows = append(windows, application.ApplicationVersion{ + Url: windows_.Url, + Version: windows_.Version, + Platform: "windows", + IsDefault: windows_.IsDefault, + Description: windows_.Description, + ApplicationId: app.Id, + }) + } + } + + var harmony []application.ApplicationVersion + if len(req.Platform.Harmony) > 0 { + for _, harmony_ := range req.Platform.Harmony { + harmony = append(harmony, application.ApplicationVersion{ + Url: harmony_.Url, + Version: harmony_.Version, + Platform: "harmony", + IsDefault: harmony_.IsDefault, + Description: harmony_.Description, + ApplicationId: app.Id, + }) + } + } + var applicationVersions []application.ApplicationVersion + applicationVersions = append(applicationVersions, ios...) + applicationVersions = append(applicationVersions, mac...) + applicationVersions = append(applicationVersions, linux...) + applicationVersions = append(applicationVersions, android...) + applicationVersions = append(applicationVersions, windows...) + applicationVersions = append(applicationVersions, harmony...) + app.ApplicationVersions = applicationVersions + err = l.svcCtx.ApplicationModel.Transaction(l.ctx, func(db *gorm.DB) error { + + if err = db.Where("application_id = ?", app.Id).Delete(&application.ApplicationVersion{}).Error; err != nil { + return err + } + if err = db.Create(&applicationVersions).Error; err != nil { + return err + } + return db.Save(app).Error + }) + if err != nil { + l.Errorw("[UpdateApplication] update application error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update application error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/system/updateApplicationVersionLogic.go b/internal/logic/admin/system/updateApplicationVersionLogic.go new file mode 100644 index 0000000..ce33db0 --- /dev/null +++ b/internal/logic/admin/system/updateApplicationVersionLogic.go @@ -0,0 +1,45 @@ +package system + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateApplicationVersionLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Update application version +func NewUpdateApplicationVersionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateApplicationVersionLogic { + return &UpdateApplicationVersionLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateApplicationVersionLogic) UpdateApplicationVersion(req *types.UpdateApplicationVersionRequest) error { + // find application + app, err := l.svcCtx.ApplicationModel.FindOneVersion(l.ctx, req.Id) + if err != nil { + l.Errorw("[UpdateApplicationVersion] find application version error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find application error: %v", err.Error()) + } + app.Url = req.Url + app.Version = req.Version + app.Description = req.Description + app.IsDefault = req.IsDefault + err = l.svcCtx.ApplicationModel.UpdateVersion(l.ctx, app) + if err != nil { + l.Errorw("[UpdateApplicationVersion] update application version error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update application version error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/system/updateCurrencyConfigLogic.go b/internal/logic/admin/system/updateCurrencyConfigLogic.go new file mode 100644 index 0000000..8bd2326 --- /dev/null +++ b/internal/logic/admin/system/updateCurrencyConfigLogic.go @@ -0,0 +1,62 @@ +package system + +import ( + "context" + "reflect" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/model/system" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type UpdateCurrencyConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Update Currency Config +func NewUpdateCurrencyConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateCurrencyConfigLogic { + return &UpdateCurrencyConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateCurrencyConfigLogic) UpdateCurrencyConfig(req *types.CurrencyConfig) error { + v := reflect.ValueOf(*req) + // Get the reflection type of the structure + t := v.Type() + err := l.svcCtx.SystemModel.Transaction(l.ctx, func(db *gorm.DB) error { + var err error + for i := 0; i < v.NumField(); i++ { + // Get the field name + fieldName := t.Field(i).Name + // Get the field value to string + fieldValue := tool.ConvertValueToString(v.Field(i)) + // Update the invite config + err = db.Model(&system.System{}).Where("`category` = 'currency' and `key` = ?", fieldName).Update("value", fieldValue).Error + if err != nil { + break + } + } + if err != nil { + return err + } + // clear cache + return l.svcCtx.Redis.Del(l.ctx, config.CurrencyConfigKey, config.GlobalConfigKey).Err() + }) + if err != nil { + l.Errorw("[UpdateCurrencyConfig] update currency config error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update invite config error: %v", err) + } + return nil +} diff --git a/internal/logic/admin/system/updateInviteConfigLogic.go b/internal/logic/admin/system/updateInviteConfigLogic.go new file mode 100644 index 0000000..8ebc555 --- /dev/null +++ b/internal/logic/admin/system/updateInviteConfigLogic.go @@ -0,0 +1,64 @@ +package system + +import ( + "context" + "reflect" + + "github.com/perfect-panel/ppanel-server/initialize" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/model/system" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" +) + +type UpdateInviteConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUpdateInviteConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateInviteConfigLogic { + return &UpdateInviteConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateInviteConfigLogic) UpdateInviteConfig(req *types.InviteConfig) error { + v := reflect.ValueOf(*req) + // Get the reflection type of the structure + t := v.Type() + err := l.svcCtx.SystemModel.Transaction(l.ctx, func(db *gorm.DB) error { + var err error + for i := 0; i < v.NumField(); i++ { + // Get the field name + fieldName := t.Field(i).Name + // Get the field value to string + fieldValue := tool.ConvertValueToString(v.Field(i)) + // Update the invite config + err = db.Model(&system.System{}).Where("`category` = 'invite' and `key` = ?", fieldName).Update("value", fieldValue).Error + if err != nil { + break + } + } + if err != nil { + return err + } + // clear cache + return l.svcCtx.Redis.Del(l.ctx, config.InviteConfigKey, config.GlobalConfigKey).Err() + }) + if err != nil { + l.Errorw("[UpdateInviteConfig] update invite config error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update invite config error: %v", err) + } + initialize.Invite(l.svcCtx) + return nil +} diff --git a/internal/logic/admin/system/updateNodeConfigLogic.go b/internal/logic/admin/system/updateNodeConfigLogic.go new file mode 100644 index 0000000..c7523de --- /dev/null +++ b/internal/logic/admin/system/updateNodeConfigLogic.go @@ -0,0 +1,59 @@ +package system + +import ( + "context" + "reflect" + + "github.com/perfect-panel/ppanel-server/initialize" + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/model/system" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" +) + +type UpdateNodeConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUpdateNodeConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateNodeConfigLogic { + return &UpdateNodeConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateNodeConfigLogic) UpdateNodeConfig(req *types.NodeConfig) error { + v := reflect.ValueOf(*req) + // Get the reflection type of the structure + t := v.Type() + err := l.svcCtx.SystemModel.Transaction(l.ctx, func(db *gorm.DB) error { + var err error + for i := 0; i < v.NumField(); i++ { + // Get the field name + fieldName := t.Field(i).Name + // Get the field value to string + fieldValue := tool.ConvertValueToString(v.Field(i)) + // Update the server config + err = db.Model(&system.System{}).Where("`category` = 'server' and `key` = ?", fieldName).Update("value", fieldValue).Error + if err != nil { + break + } + } + return l.svcCtx.Redis.Del(l.ctx, config.NodeConfigKey).Err() + }) + if err != nil { + l.Errorw("[UpdateNodeConfig] update node config error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update server config error: %v", err) + } + initialize.Node(l.svcCtx) + return nil +} diff --git a/internal/logic/admin/system/updatePrivacyPolicyConfigLogic.go b/internal/logic/admin/system/updatePrivacyPolicyConfigLogic.go new file mode 100644 index 0000000..49f5122 --- /dev/null +++ b/internal/logic/admin/system/updatePrivacyPolicyConfigLogic.go @@ -0,0 +1,59 @@ +package system + +import ( + "context" + "reflect" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/model/system" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type UpdatePrivacyPolicyConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Update Privacy Policy Config +func NewUpdatePrivacyPolicyConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdatePrivacyPolicyConfigLogic { + return &UpdatePrivacyPolicyConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdatePrivacyPolicyConfigLogic) UpdatePrivacyPolicyConfig(req *types.PrivacyPolicyConfig) error { + v := reflect.ValueOf(*req) + // Get the reflection type of the structure + t := v.Type() + err := l.svcCtx.SystemModel.Transaction(l.ctx, func(db *gorm.DB) error { + var err error + for i := 0; i < v.NumField(); i++ { + // Get the field name + fieldName := t.Field(i).Name + // Get the field value to string + fieldValue := tool.ConvertValueToString(v.Field(i)) + // Update the tos config + err = db.Model(&system.System{}).Where("`category` = 'tos' and `key` = ?", fieldName).Update("value", fieldValue).Error + if err != nil { + break + } + } + return l.svcCtx.Redis.Del(l.ctx, config.TosConfigKey).Err() + }) + if err != nil { + l.Errorw("[UpdateTosConfigLogic] update tos config error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update tos config error: %v", err) + } + + return nil +} diff --git a/internal/logic/admin/system/updateRegisterConfigLogic.go b/internal/logic/admin/system/updateRegisterConfigLogic.go new file mode 100644 index 0000000..b990d4c --- /dev/null +++ b/internal/logic/admin/system/updateRegisterConfigLogic.go @@ -0,0 +1,65 @@ +package system + +import ( + "context" + + "reflect" + + "github.com/perfect-panel/ppanel-server/initialize" + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/model/system" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" +) + +type UpdateRegisterConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUpdateRegisterConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateRegisterConfigLogic { + return &UpdateRegisterConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateRegisterConfigLogic) UpdateRegisterConfig(req *types.RegisterConfig) error { + v := reflect.ValueOf(*req) + // Get the reflection type of the structure + t := v.Type() + err := l.svcCtx.SystemModel.Transaction(l.ctx, func(db *gorm.DB) error { + var err error + for i := 0; i < v.NumField(); i++ { + // Get the field name + fieldName := t.Field(i).Name + // Get the field value to string + fieldValue := tool.ConvertValueToString(v.Field(i)) + // Update the site config + err = db.Model(&system.System{}).Where("`category` = 'register' and `key` = ?", fieldName).Update("value", fieldValue).Error + if err != nil { + break + } + } + if err != nil { + return err + } + return l.svcCtx.Redis.Del(l.ctx, config.RegisterConfigKey, config.GlobalConfigKey).Err() + }) + if err != nil { + l.Errorw("[UpdateRegisterConfig] update register config error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update register config error: %v", err.Error()) + } + // init system config + initialize.Register(l.svcCtx) + return nil +} diff --git a/internal/logic/admin/system/updateSiteConfigLogic.go b/internal/logic/admin/system/updateSiteConfigLogic.go new file mode 100644 index 0000000..3e8a3cc --- /dev/null +++ b/internal/logic/admin/system/updateSiteConfigLogic.go @@ -0,0 +1,61 @@ +package system + +import ( + "context" + "reflect" + + "github.com/perfect-panel/ppanel-server/initialize" + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/model/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type UpdateSiteConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUpdateSiteConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateSiteConfigLogic { + return &UpdateSiteConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateSiteConfigLogic) UpdateSiteConfig(req *types.SiteConfig) error { + // Get the reflection value of the structure + v := reflect.ValueOf(*req) + // Get the reflection type of the structure + t := v.Type() + err := l.svcCtx.SystemModel.Transaction(l.ctx, func(db *gorm.DB) error { + var err error + for i := 0; i < v.NumField(); i++ { + // Get the field name + fieldName := t.Field(i).Name + // Get the field value + fieldValue := v.Field(i) + err = db.Model(&system.System{}).Where("`category` = 'site' and `key` = ?", fieldName).Update("value", fieldValue.String()).Error + if err != nil { + break + } + } + if err != nil { + return err + } + + return l.svcCtx.Redis.Del(l.ctx, config.SiteConfigKey, config.GlobalConfigKey).Err() + }) + if err != nil { + l.Logger.Error("[UpdateSiteConfig] update site config error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update site config error: %v", err.Error()) + } + initialize.Site(l.svcCtx) + return nil +} diff --git a/internal/logic/admin/system/updateSubscribeConfigLogic.go b/internal/logic/admin/system/updateSubscribeConfigLogic.go new file mode 100644 index 0000000..69b1436 --- /dev/null +++ b/internal/logic/admin/system/updateSubscribeConfigLogic.go @@ -0,0 +1,70 @@ +package system + +import ( + "context" + "reflect" + + "github.com/perfect-panel/ppanel-server/initialize" + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/model/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type UpdateSubscribeConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUpdateSubscribeConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateSubscribeConfigLogic { + return &UpdateSubscribeConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateSubscribeConfigLogic) UpdateSubscribeConfig(req *types.SubscribeConfig) error { + v := reflect.ValueOf(*req) + // Get the reflection type of the structure + t := v.Type() + err := l.svcCtx.SystemModel.Transaction(l.ctx, func(db *gorm.DB) error { + var err error + for i := 0; i < v.NumField(); i++ { + // Get the field name + fieldName := t.Field(i).Name + // Get the field value to string + fieldValue := tool.ConvertValueToString(v.Field(i)) + // Update the site config + err = db.Model(&system.System{}).Where("`category` = 'subscribe' and `key` = ?", fieldName).Update("value", fieldValue).Error + if err != nil { + break + } + } + return l.svcCtx.Redis.Del(l.ctx, config.SubscribeConfigKey, config.GlobalConfigKey).Err() + }) + + if err != nil { + l.Errorw("[UpdateSubscribeConfigLogic] update subscribe config error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update subscribe config error: %v", err) + } + + if l.svcCtx.Config.Subscribe.SubscribePath != req.SubscribePath { + go func(svc *svc.ServiceContext) { + err = svc.Restart() + if err != nil { + l.Errorw("[UpdateSubscribeConfigLogic] restart error: ", logger.Field("error", err.Error())) + } + }(l.svcCtx) + return nil + } + + initialize.Subscribe(l.svcCtx) + return nil +} diff --git a/internal/logic/admin/system/updateTosConfigLogic.go b/internal/logic/admin/system/updateTosConfigLogic.go new file mode 100644 index 0000000..39d3b5e --- /dev/null +++ b/internal/logic/admin/system/updateTosConfigLogic.go @@ -0,0 +1,57 @@ +package system + +import ( + "context" + "reflect" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/model/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type UpdateTosConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUpdateTosConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateTosConfigLogic { + return &UpdateTosConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateTosConfigLogic) UpdateTosConfig(req *types.TosConfig) error { + v := reflect.ValueOf(*req) + // Get the reflection type of the structure + t := v.Type() + err := l.svcCtx.SystemModel.Transaction(l.ctx, func(db *gorm.DB) error { + var err error + for i := 0; i < v.NumField(); i++ { + // Get the field name + fieldName := t.Field(i).Name + // Get the field value to string + fieldValue := tool.ConvertValueToString(v.Field(i)) + // Update the tos config + err = db.Model(&system.System{}).Where("`category` = 'tos' and `key` = ?", fieldName).Update("value", fieldValue).Error + if err != nil { + break + } + } + return l.svcCtx.Redis.Del(l.ctx, config.TosConfigKey).Err() + }) + if err != nil { + l.Errorw("[UpdateTosConfigLogic] update tos config error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update tos config error: %v", err) + } + + return nil +} diff --git a/internal/logic/admin/system/updateVerifyCodeConfigLogic.go b/internal/logic/admin/system/updateVerifyCodeConfigLogic.go new file mode 100644 index 0000000..2838b24 --- /dev/null +++ b/internal/logic/admin/system/updateVerifyCodeConfigLogic.go @@ -0,0 +1,60 @@ +package system + +import ( + "context" + "reflect" + + "github.com/perfect-panel/ppanel-server/internal/config" + + "github.com/perfect-panel/ppanel-server/internal/model/system" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type UpdateVerifyCodeConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Update Verify Code Config +func NewUpdateVerifyCodeConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateVerifyCodeConfigLogic { + return &UpdateVerifyCodeConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateVerifyCodeConfigLogic) UpdateVerifyCodeConfig(req *types.VerifyCodeConfig) error { + v := reflect.ValueOf(*req) + // Get the reflection type of the structure + t := v.Type() + err := l.svcCtx.SystemModel.Transaction(l.ctx, func(db *gorm.DB) error { + var err error + for i := 0; i < v.NumField(); i++ { + // Get the field name + fieldName := t.Field(i).Name + // Get the field value to string + fieldValue := tool.ConvertValueToString(v.Field(i)) + // Update the site config + err = db.Model(&system.System{}).Where("`category` = 'verify_code' and `key` = ?", fieldName).Update("value", fieldValue).Error + if err != nil { + break + } + } + err = l.svcCtx.Redis.Del(l.ctx, config.VerifyCodeConfigKey).Err() + return err + }) + if err != nil { + l.Errorw("[UpdateRegisterConfig] update verify code config error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update register config error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/system/updateVerifyConfigLogic.go b/internal/logic/admin/system/updateVerifyConfigLogic.go new file mode 100644 index 0000000..5ba7543 --- /dev/null +++ b/internal/logic/admin/system/updateVerifyConfigLogic.go @@ -0,0 +1,64 @@ +package system + +import ( + "context" + "reflect" + + "github.com/perfect-panel/ppanel-server/initialize" + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/model/system" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type UpdateVerifyConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUpdateVerifyConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateVerifyConfigLogic { + return &UpdateVerifyConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateVerifyConfigLogic) UpdateVerifyConfig(req *types.VerifyConfig) error { + v := reflect.ValueOf(*req) + // Get the reflection type of the structure + t := v.Type() + err := l.svcCtx.SystemModel.Transaction(l.ctx, func(db *gorm.DB) error { + var err error + for i := 0; i < v.NumField(); i++ { + // Get the field name + fieldName := t.Field(i).Name + // Get the field value to string + fieldValue := tool.ConvertValueToString(v.Field(i)) + // Update the site config + err = db.Model(&system.System{}).Where("`category` = 'verify' and `key` = ?", fieldName).Update("value", fieldValue).Error + if err != nil { + break + } + } + if err != nil { + return err + } + // clear cache + return l.svcCtx.Redis.Del(l.ctx, config.VerifyConfigKey, config.GlobalConfigKey).Err() + }) + if err != nil { + l.Errorw("[UpdateVerifyConfigLogic] update verify config error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update verify config error: %v", err) + } + // Update the config + tool.DeepCopy(&l.svcCtx.Config.Verify, req) + initialize.Verify(l.svcCtx) + return nil +} diff --git a/internal/logic/admin/ticket/constant.go b/internal/logic/admin/ticket/constant.go new file mode 100644 index 0000000..2656fe7 --- /dev/null +++ b/internal/logic/admin/ticket/constant.go @@ -0,0 +1 @@ +package ticket diff --git a/internal/logic/admin/ticket/createTicketFollowLogic.go b/internal/logic/admin/ticket/createTicketFollowLogic.go new file mode 100644 index 0000000..93b3373 --- /dev/null +++ b/internal/logic/admin/ticket/createTicketFollowLogic.go @@ -0,0 +1,52 @@ +package ticket + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/ticket" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type CreateTicketFollowLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Create ticket follow +func NewCreateTicketFollowLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateTicketFollowLogic { + return &CreateTicketFollowLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateTicketFollowLogic) CreateTicketFollow(req *types.CreateTicketFollowRequest) (err error) { + // find ticket + _, err = l.svcCtx.TicketModel.FindOne(l.ctx, req.TicketId) + if err != nil { + l.Errorw("[CreateTicketFollow] FindOne error", logger.Field("error", err.Error()), logger.Field("ticketId", req.TicketId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find ticket failed: %v", err.Error()) + } + err = l.svcCtx.TicketModel.InsertTicketFollow(l.ctx, &ticket.Follow{ + TicketId: req.TicketId, + From: req.From, + Type: req.Type, + Content: req.Content, + }) + if err != nil { + l.Errorw("[CreateTicketFollow] Database insert error", logger.Field("error", err.Error()), logger.Field("request", req)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create ticket follow failed: %v", err.Error()) + } + err = l.svcCtx.TicketModel.UpdateTicketStatus(l.ctx, req.TicketId, 0, ticket.Waiting) + if err != nil { + l.Errorw("[CreateTicketFollow] Database update error", logger.Field("error", err.Error()), logger.Field("status", ticket.Waiting)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update ticket status failed: %v", err.Error()) + } + return +} diff --git a/internal/logic/admin/ticket/getTicketListLogic.go b/internal/logic/admin/ticket/getTicketListLogic.go new file mode 100644 index 0000000..0a16273 --- /dev/null +++ b/internal/logic/admin/ticket/getTicketListLogic.go @@ -0,0 +1,41 @@ +package ticket + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetTicketListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get ticket list +func NewGetTicketListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetTicketListLogic { + return &GetTicketListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetTicketListLogic) GetTicketList(req *types.GetTicketListRequest) (resp *types.GetTicketListResponse, err error) { + total, list, err := l.svcCtx.TicketModel.QueryTicketList(l.ctx, int(req.Page), int(req.Size), req.UserId, req.Status, req.Search) + if err != nil { + l.Errorw("[GetTicketList] Query Database Error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryTicketList error: %v", err) + } + resp = &types.GetTicketListResponse{ + Total: total, + List: make([]types.Ticket, 0), + } + tool.DeepCopy(&resp.List, list) + return +} diff --git a/internal/logic/admin/ticket/getTicketLogic.go b/internal/logic/admin/ticket/getTicketLogic.go new file mode 100644 index 0000000..7aaa826 --- /dev/null +++ b/internal/logic/admin/ticket/getTicketLogic.go @@ -0,0 +1,38 @@ +package ticket + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetTicketLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get ticket detail +func NewGetTicketLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetTicketLogic { + return &GetTicketLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetTicketLogic) GetTicket(req *types.GetTicketRequest) (resp *types.Ticket, err error) { + data, err := l.svcCtx.TicketModel.QueryTicketDetail(l.ctx, req.Id) + if err != nil { + l.Errorw("[GetTicket] Query Database Error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get ticket detail failed: %v", err.Error()) + } + resp = &types.Ticket{} + tool.DeepCopy(resp, data) + return +} diff --git a/internal/logic/admin/ticket/updateTicketStatusLogic.go b/internal/logic/admin/ticket/updateTicketStatusLogic.go new file mode 100644 index 0000000..c205057 --- /dev/null +++ b/internal/logic/admin/ticket/updateTicketStatusLogic.go @@ -0,0 +1,36 @@ +package ticket + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateTicketStatusLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Update ticket status +func NewUpdateTicketStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateTicketStatusLogic { + return &UpdateTicketStatusLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateTicketStatusLogic) UpdateTicketStatus(req *types.UpdateTicketStatusRequest) error { + + err := l.svcCtx.TicketModel.UpdateTicketStatus(l.ctx, req.Id, 0, *req.Status) + if err != nil { + l.Errorw("[UpdateTicketStatus] Update Database Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update ticket error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/tool/getSystemLogLogic.go b/internal/logic/admin/tool/getSystemLogLogic.go new file mode 100644 index 0000000..e2d4660 --- /dev/null +++ b/internal/logic/admin/tool/getSystemLogLogic.go @@ -0,0 +1,40 @@ +package tool + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type GetSystemLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get System Log +func NewGetSystemLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSystemLogLogic { + return &GetSystemLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetSystemLogLogic) GetSystemLog() (resp *types.LogResponse, err error) { + //if l.svcCtx.Config.Debug { + // return nil, errors.Wrapf(xerr.NewErrCode(xerr.DebugModeError), "debug mode is enabled") + //} + //lines, err := logger.ReadLastNLines(l.svcCtx.Config.Logger.FilePath, 100) + //if err != nil { + // l.Errorw("[GetSystemLog]", logger.Field("error", "ReadLastNLines"), logger.Field(err)) + // return nil, err + //} + //logs := logger.ParseLog(lines) + //return &types.LogResponse{ + // List: logs, + //}, nil + return nil, nil +} diff --git a/internal/logic/admin/tool/restartSystemLogic.go b/internal/logic/admin/tool/restartSystemLogic.go new file mode 100644 index 0000000..929ae70 --- /dev/null +++ b/internal/logic/admin/tool/restartSystemLogic.go @@ -0,0 +1,35 @@ +package tool + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type RestartSystemLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Restart System +func NewRestartSystemLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RestartSystemLogic { + return &RestartSystemLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *RestartSystemLogic) RestartSystem() error { + l.Logger.Info("[RestartSystem]", logger.Field("info", "Restarting system")) + go func() { + err := l.svcCtx.Restart() + if err != nil { + l.Errorw("[RestartSystem]", logger.Field("error", err.Error())) + } + l.Logger.Info("[RestartSystem]", logger.Field("info", "System restarted")) + }() + return nil +} diff --git a/internal/logic/admin/user/batchDeleteUserLogic.go b/internal/logic/admin/user/batchDeleteUserLogic.go new file mode 100644 index 0000000..c7781a1 --- /dev/null +++ b/internal/logic/admin/user/batchDeleteUserLogic.go @@ -0,0 +1,34 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type BatchDeleteUserLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logger.Logger +} + +func NewBatchDeleteUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BatchDeleteUserLogic { + return &BatchDeleteUserLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logger.WithContext(ctx), + } +} + +func (l *BatchDeleteUserLogic) BatchDeleteUser(req *types.BatchDeleteUserRequest) error { + err := l.svcCtx.UserModel.BatchDeleteUser(l.ctx, req.Ids) + if err != nil { + l.Logger.Error("[BatchDeleteUserLogic] BatchDeleteUser failed: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "BatchDeleteUser failed: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/user/createUserAuthMethodLogic.go b/internal/logic/admin/user/createUserAuthMethodLogic.go new file mode 100644 index 0000000..a9bd1e1 --- /dev/null +++ b/internal/logic/admin/user/createUserAuthMethodLogic.go @@ -0,0 +1,50 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type CreateUserAuthMethodLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Create user auth method +func NewCreateUserAuthMethodLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateUserAuthMethodLogic { + return &CreateUserAuthMethodLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateUserAuthMethodLogic) CreateUserAuthMethod(req *types.CreateUserAuthMethodRequest) error { + err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + var data *user.AuthMethods + if err := db.Model(&user.AuthMethods{}).Where("`user_id` = ? AND `auth_type` = ?", req.UserId, req.AuthType).First(&data).Error; err != nil && errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + data.UserId = req.UserId + data.AuthType = req.AuthType + data.AuthIdentifier = req.AuthIdentifier + if err := db.Model(&user.AuthMethods{}).Save(&data).Error; err != nil { + return err + } + return nil + }) + if err != nil { + l.Errorw("[CreateUserAuthMethodLogic] Create User Auth Method Error:", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "Create User Auth Method Error") + } + return nil +} diff --git a/internal/logic/admin/user/createUserLogic.go b/internal/logic/admin/user/createUserLogic.go new file mode 100644 index 0000000..3cd97a4 --- /dev/null +++ b/internal/logic/admin/user/createUserLogic.go @@ -0,0 +1,91 @@ +package user + +import ( + "context" + "fmt" + "time" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/uuidx" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type CreateUserLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logger.Logger +} + +func NewCreateUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateUserLogic { + return &CreateUserLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logger.WithContext(ctx), + } +} +func (l *CreateUserLogic) CreateUser(req *types.CreateUserRequest) error { + if req.ReferCode == "" { + // timestamp replaces user id + req.ReferCode = uuidx.UserInviteCode(time.Now().UnixMicro()) + } + if req.Password == "" { + req.Password = req.Email + } + pwd := tool.EncodePassWord(req.Password) + newUser := &user.User{ + Password: pwd, + ReferCode: req.ReferCode, + Balance: req.Balance, + IsAdmin: &req.IsAdmin, + } + var ams []user.AuthMethods + + if req.TelephoneAreaCode != "" && req.Telephone != "" { + phone := fmt.Sprintf("%s-%s", req.TelephoneAreaCode, req.Telephone) + _, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "mobile", phone) + if err == nil { + return errors.Wrapf(xerr.NewErrCode(xerr.TelephoneExist), "telephone exist") + } + ams = append(ams, user.AuthMethods{ + AuthType: "mobile", + AuthIdentifier: phone, + }) + } + if req.Email != "" { + _, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "email", req.Email) + if err == nil { + return errors.Wrapf(xerr.NewErrCode(xerr.EmailExist), "email exist") + } + ams = append(ams, user.AuthMethods{ + AuthType: "email", + AuthIdentifier: req.Email, + }) + } + + newUser.AuthMethods = ams + + // todo: get product id and duration + if req.RefererUser != "" { + // get referer user id + u, err := l.svcCtx.UserModel.FindOneByEmail(l.ctx, req.RefererUser) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "referer user not found: %v", err.Error()) + } + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find referer user failed: %v", err.Error()) + } + newUser.RefererId = u.Id + } + + err := l.svcCtx.UserModel.Insert(l.ctx, newUser) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert user failed: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/user/createUserSubscribeLogic.go b/internal/logic/admin/user/createUserSubscribeLogic.go new file mode 100644 index 0000000..dbaff74 --- /dev/null +++ b/internal/logic/admin/user/createUserSubscribeLogic.go @@ -0,0 +1,81 @@ +package user + +import ( + "context" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/uuidx" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type CreateUserSubscribeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Create user subcribe +func NewCreateUserSubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateUserSubscribeLogic { + return &CreateUserSubscribeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateUserSubscribeLogic) CreateUserSubscribe(req *types.CreateUserSubscribeRequest) error { + // validate user + userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, req.UserId) + if err != nil { + l.Errorw("FindOne error", logger.Field("error", err.Error()), logger.Field("userId", req.UserId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOne error: %v", err.Error()) + } + subs, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, req.UserId) + if err != nil { + l.Errorw("QueryUserSubscribe error", logger.Field("error", err.Error()), logger.Field("userId", req.UserId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryUserSubscribe error: %v", err.Error()) + } + if len(subs) >= 1 && l.svcCtx.Config.Subscribe.SingleModel { + return errors.Wrapf(xerr.NewErrCode(xerr.SingleSubscribeModeExceedsLimit), "Single subscribe mode exceeds limit") + } + sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribeId) + if err != nil { + l.Errorw("FindOne error", logger.Field("error", err.Error()), logger.Field("subscribeId", req.SubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOne error: %v", err.Error()) + } + if req.Traffic == 0 { + req.Traffic = sub.Traffic + } + + userSub := user.Subscribe{ + UserId: req.UserId, + SubscribeId: req.SubscribeId, + StartTime: time.Now(), + ExpireTime: time.UnixMilli(req.ExpiredAt), + Traffic: req.Traffic, + Download: 0, + Upload: 0, + Token: uuidx.SubscribeToken(fmt.Sprintf("adminCreate:%d", time.Now().UnixMilli())), + UUID: uuid.New().String(), + Status: 1, + } + if err = l.svcCtx.UserModel.InsertSubscribe(l.ctx, &userSub); err != nil { + l.Errorw("InsertSubscribe error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "InsertSubscribe error: %v", err.Error()) + } + + err = l.svcCtx.UserModel.UpdateUserCache(l.ctx, userInfo) + if err != nil { + l.Errorw("UpdateUserCache error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "UpdateUserCache error: %v", err.Error()) + } + + return nil +} diff --git a/internal/logic/admin/user/currentUserLogic.go b/internal/logic/admin/user/currentUserLogic.go new file mode 100644 index 0000000..28b2727 --- /dev/null +++ b/internal/logic/admin/user/currentUserLogic.go @@ -0,0 +1,44 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type CurrentUserLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logger.Logger +} + +func NewCurrentUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CurrentUserLogic { + return &CurrentUserLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logger.WithContext(ctx), + } +} + +func (l *CurrentUserLogic) CurrentUser() (*types.User, error) { + resp := &types.User{} + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + + l.Logger.Info("current user", zap.Field{Key: "userId", Type: zapcore.Int64Type, Integer: u.Id}) + tool.DeepCopy(resp, u) + return resp, nil +} diff --git a/internal/logic/admin/user/deleteUserAuthMethodLogic.go b/internal/logic/admin/user/deleteUserAuthMethodLogic.go new file mode 100644 index 0000000..38692ea --- /dev/null +++ b/internal/logic/admin/user/deleteUserAuthMethodLogic.go @@ -0,0 +1,35 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type DeleteUserAuthMethodLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Delete user auth method +func NewDeleteUserAuthMethodLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteUserAuthMethodLogic { + return &DeleteUserAuthMethodLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteUserAuthMethodLogic) DeleteUserAuthMethod(req *types.DeleteUserAuthMethodRequest) error { + err := l.svcCtx.UserModel.DeleteUserAuthMethods(l.ctx, req.UserId, req.AuthType) + if err != nil { + l.Errorw("[DeleteUserAuthMethodLogic] Delete User Auth Method Error:", logger.Field("err", err.Error()), logger.Field("userId", req.UserId), logger.Field("authType", req.AuthType)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "Delete User Auth Method Error") + } + return nil +} diff --git a/internal/logic/admin/user/deleteUserDeviceLogic.go b/internal/logic/admin/user/deleteUserDeviceLogic.go new file mode 100644 index 0000000..101b33b --- /dev/null +++ b/internal/logic/admin/user/deleteUserDeviceLogic.go @@ -0,0 +1,35 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type DeleteUserDeviceLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Delete user device +func NewDeleteUserDeviceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteUserDeviceLogic { + return &DeleteUserDeviceLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteUserDeviceLogic) DeleteUserDevice(req *types.DeleteUserDeivceRequest) error { + err := l.svcCtx.UserModel.DeleteDevice(l.ctx, req.Id) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete user error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/user/deleteUserLogic.go b/internal/logic/admin/user/deleteUserLogic.go new file mode 100644 index 0000000..d283058 --- /dev/null +++ b/internal/logic/admin/user/deleteUserLogic.go @@ -0,0 +1,33 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type DeleteUserLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logger.Logger +} + +func NewDeleteUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteUserLogic { + return &DeleteUserLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logger.WithContext(ctx), + } +} + +func (l *DeleteUserLogic) DeleteUser(req *types.GetDetailRequest) error { + err := l.svcCtx.UserModel.Delete(l.ctx, req.Id) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete user error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/user/deleteUserSubscribeLogic.go b/internal/logic/admin/user/deleteUserSubscribeLogic.go new file mode 100644 index 0000000..463638e --- /dev/null +++ b/internal/logic/admin/user/deleteUserSubscribeLogic.go @@ -0,0 +1,35 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type DeleteUserSubscribeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewDeleteUserSubscribeLogic Delete user subcribe +func NewDeleteUserSubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteUserSubscribeLogic { + return &DeleteUserSubscribeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteUserSubscribeLogic) DeleteUserSubscribe(req *types.DeleteUserSubscribeRequest) error { + err := l.svcCtx.UserModel.DeleteSubscribeById(l.ctx, req.UserSubscribeId) + if err != nil { + l.Errorw("failed to delete user subscribe", logger.Field("error", err.Error()), logger.Field("userSubscribeId", req.UserSubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "failed to delete user subscribe: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/user/getUserAuthMethodLogic.go b/internal/logic/admin/user/getUserAuthMethodLogic.go new file mode 100644 index 0000000..a7f8740 --- /dev/null +++ b/internal/logic/admin/user/getUserAuthMethodLogic.go @@ -0,0 +1,41 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetUserAuthMethodLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get user auth method +func NewGetUserAuthMethodLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserAuthMethodLogic { + return &GetUserAuthMethodLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetUserAuthMethodLogic) GetUserAuthMethod(req *types.GetUserAuthMethodRequest) (resp *types.GetUserAuthMethodResponse, err error) { + methods, err := l.svcCtx.UserModel.FindUserAuthMethods(l.ctx, req.UserId) + if err != nil { + l.Errorw("[GetUserAuthMethodLogic] Get User Auth Method Error:", logger.Field("err", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Get User Auth Method Error") + } + list := make([]types.UserAuthMethod, 0) + tool.DeepCopy(&list, methods) + + return &types.GetUserAuthMethodResponse{ + AuthMethods: list, + }, nil +} diff --git a/internal/logic/admin/user/getUserDetailLogic.go b/internal/logic/admin/user/getUserDetailLogic.go new file mode 100644 index 0000000..ab4bfd7 --- /dev/null +++ b/internal/logic/admin/user/getUserDetailLogic.go @@ -0,0 +1,36 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetUserDetailLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logger.Logger +} + +func NewGetUserDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserDetailLogic { + return &GetUserDetailLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logger.WithContext(ctx), + } +} + +func (l *GetUserDetailLogic) GetUserDetail(req *types.GetDetailRequest) (*types.User, error) { + resp := types.User{} + userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get user detail error: %v", err.Error()) + } + tool.DeepCopy(&resp, userInfo) + return &resp, nil +} diff --git a/internal/logic/admin/user/getUserListLogic.go b/internal/logic/admin/user/getUserListLogic.go new file mode 100644 index 0000000..145cba9 --- /dev/null +++ b/internal/logic/admin/user/getUserListLogic.go @@ -0,0 +1,63 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/phone" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetUserListLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logger.Logger +} + +func NewGetUserListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserListLogic { + return &GetUserListLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logger.WithContext(ctx), + } +} +func (l *GetUserListLogic) GetUserList(req *types.GetUserListRequest) (*types.GetUserListResponse, error) { + list, total, err := l.svcCtx.UserModel.QueryPageList(l.ctx, req.Page, req.Size, &user.UserFilterParams{ + UserId: req.UserId, + Search: req.Search, + SubscribeId: req.SubscribeId, + UserSubscribeId: req.UserSubscribeId, + }) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetUserListLogic failed: %v", err.Error()) + } + + userRespList := make([]types.User, 0, len(list)) + + for _, item := range list { + var user types.User + tool.DeepCopy(&user, item) + + // 处理 AuthMethods + authMethods := make([]types.UserAuthMethod, len(user.AuthMethods)) // 直接创建目标 slice + for i, method := range user.AuthMethods { + tool.DeepCopy(&authMethods[i], method) + if method.AuthType == "mobile" { + authMethods[i].AuthIdentifier = phone.FormatToInternational(method.AuthIdentifier) + } + } + user.AuthMethods = authMethods + + userRespList = append(userRespList, user) + } + + return &types.GetUserListResponse{ + Total: total, + List: userRespList, + }, nil +} diff --git a/internal/logic/admin/user/getUserLoginLogsLogic.go b/internal/logic/admin/user/getUserLoginLogsLogic.go new file mode 100644 index 0000000..46ccc94 --- /dev/null +++ b/internal/logic/admin/user/getUserLoginLogsLogic.go @@ -0,0 +1,44 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetUserLoginLogsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get user login logs +func NewGetUserLoginLogsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserLoginLogsLogic { + return &GetUserLoginLogsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetUserLoginLogsLogic) GetUserLoginLogs(req *types.GetUserLoginLogsRequest) (resp *types.GetUserLoginLogsResponse, err error) { + data, total, err := l.svcCtx.UserModel.FilterLoginLogList(l.ctx, req.Page, req.Size, &user.LoginLogFilterParams{ + UserId: req.UserId, + }) + if err != nil { + l.Errorw("[GetUserLoginLogs] get user login logs failed", logger.Field("error", err.Error()), logger.Field("request", req)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get user login logs failed: %v", err.Error()) + } + var list []types.UserLoginLog + tool.DeepCopy(&list, data) + return &types.GetUserLoginLogsResponse{ + Total: total, + List: list, + }, nil +} diff --git a/internal/logic/admin/user/getUserSubscribeByIdLogic.go b/internal/logic/admin/user/getUserSubscribeByIdLogic.go new file mode 100644 index 0000000..6a4a454 --- /dev/null +++ b/internal/logic/admin/user/getUserSubscribeByIdLogic.go @@ -0,0 +1,38 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetUserSubscribeByIdLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get user subcribe by id +func NewGetUserSubscribeByIdLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserSubscribeByIdLogic { + return &GetUserSubscribeByIdLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetUserSubscribeByIdLogic) GetUserSubscribeById(req *types.GetUserSubscribeByIdRequest) (resp *types.UserSubscribeDetail, err error) { + sub, err := l.svcCtx.UserModel.FindOneSubscribeDetailsById(l.ctx, req.Id) + if err != nil { + l.Errorw("[GetUserSubscribeByIdLogic] FindOneSubscribeDetailsById error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneSubscribeDetailsById error: %v", err.Error()) + } + var subscribeDetails types.UserSubscribeDetail + tool.DeepCopy(&subscribeDetails, sub) + return &subscribeDetails, nil +} diff --git a/internal/logic/admin/user/getUserSubscribeDevicesLogic.go b/internal/logic/admin/user/getUserSubscribeDevicesLogic.go new file mode 100644 index 0000000..349152f --- /dev/null +++ b/internal/logic/admin/user/getUserSubscribeDevicesLogic.go @@ -0,0 +1,41 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type GetUserSubscribeDevicesLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get user subcribe devices +func NewGetUserSubscribeDevicesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserSubscribeDevicesLogic { + return &GetUserSubscribeDevicesLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetUserSubscribeDevicesLogic) GetUserSubscribeDevices(req *types.GetUserSubscribeDevicesRequest) (resp *types.GetUserSubscribeDevicesResponse, err error) { + list, total, err := l.svcCtx.UserModel.QueryDevicePageList(l.ctx, req.UserId, req.SubscribeId, req.Page, req.Size) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetUserSubscribeDevices failed: %v", err.Error()) + } + userRespList := make([]types.UserDevice, 0) + tool.DeepCopy(&userRespList, list) + return &types.GetUserSubscribeDevicesResponse{ + Total: total, + List: userRespList, + }, nil +} diff --git a/internal/logic/admin/user/getUserSubscribeLogic.go b/internal/logic/admin/user/getUserSubscribeLogic.go new file mode 100644 index 0000000..abf8dfc --- /dev/null +++ b/internal/logic/admin/user/getUserSubscribeLogic.go @@ -0,0 +1,47 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetUserSubscribeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get user subcribe +func NewGetUserSubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserSubscribeLogic { + return &GetUserSubscribeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetUserSubscribeLogic) GetUserSubscribe(req *types.GetUserSubscribeListRequest) (resp *types.GetUserSubscribeListResponse, err error) { + data, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, req.UserId, 0, 1, 2, 3, 4) + if err != nil { + l.Errorw("[GetUserSubscribeLogs] Get User Subscribe Error:", logger.Field("err", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Get User Subscribe Error") + } + + resp = &types.GetUserSubscribeListResponse{ + List: make([]types.UserSubscribe, 0), + Total: int64(len(data)), + } + + for _, item := range data { + var sub types.UserSubscribe + tool.DeepCopy(&sub, item) + resp.List = append(resp.List, sub) + } + return +} diff --git a/internal/logic/admin/user/getUserSubscribeLogsLogic.go b/internal/logic/admin/user/getUserSubscribeLogsLogic.go new file mode 100644 index 0000000..01b8135 --- /dev/null +++ b/internal/logic/admin/user/getUserSubscribeLogsLogic.go @@ -0,0 +1,47 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetUserSubscribeLogsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get user subcribe logs +func NewGetUserSubscribeLogsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserSubscribeLogsLogic { + return &GetUserSubscribeLogsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetUserSubscribeLogsLogic) GetUserSubscribeLogs(req *types.GetUserSubscribeLogsRequest) (resp *types.GetUserSubscribeLogsResponse, err error) { + data, total, err := l.svcCtx.UserModel.FilterSubscribeLogList(l.ctx, req.Page, req.Size, &user.SubscribeLogFilterParams{ + UserSubscribeId: req.SubscribeId, + UserId: req.UserId, + }) + + if err != nil { + l.Errorw("[GetUserSubscribeLogs] Get User Subscribe Logs Error:", logger.Field("err", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Get User Subscribe Logs Error") + } + var list []types.UserSubscribeLog + tool.DeepCopy(&list, data) + + return &types.GetUserSubscribeLogsResponse{ + List: list, + Total: total, + }, err +} diff --git a/internal/logic/admin/user/getUserSubscribeTrafficLogsLogic.go b/internal/logic/admin/user/getUserSubscribeTrafficLogsLogic.go new file mode 100644 index 0000000..782b96f --- /dev/null +++ b/internal/logic/admin/user/getUserSubscribeTrafficLogsLogic.go @@ -0,0 +1,41 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type GetUserSubscribeTrafficLogsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get user subcribe traffic logs +func NewGetUserSubscribeTrafficLogsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserSubscribeTrafficLogsLogic { + return &GetUserSubscribeTrafficLogsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetUserSubscribeTrafficLogsLogic) GetUserSubscribeTrafficLogs(req *types.GetUserSubscribeTrafficLogsRequest) (resp *types.GetUserSubscribeTrafficLogsResponse, err error) { + list, total, err := l.svcCtx.TrafficLogModel.QueryTrafficLogPageList(l.ctx, req.UserId, req.SubscribeId, req.Page, req.Size) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetUserSubscribeTrafficLogs failed: %v", err.Error()) + } + userRespList := make([]types.TrafficLog, 0) + tool.DeepCopy(&userRespList, list) + return &types.GetUserSubscribeTrafficLogsResponse{ + Total: total, + List: userRespList, + }, nil +} diff --git a/internal/logic/admin/user/kickOfflineByUserDeviceLogic.go b/internal/logic/admin/user/kickOfflineByUserDeviceLogic.go new file mode 100644 index 0000000..dafec89 --- /dev/null +++ b/internal/logic/admin/user/kickOfflineByUserDeviceLogic.go @@ -0,0 +1,42 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type KickOfflineByUserDeviceLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// kick offline user device +func NewKickOfflineByUserDeviceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *KickOfflineByUserDeviceLogic { + return &KickOfflineByUserDeviceLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *KickOfflineByUserDeviceLogic) KickOfflineByUserDevice(req *types.KickOfflineRequest) error { + device, err := l.svcCtx.UserModel.FindOneDevice(l.ctx, req.Id) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get Device error: %v", err.Error()) + } + l.svcCtx.DeviceManager.KickDevice(device.UserId, device.Identifier) + device.Online = false + err = l.svcCtx.UserModel.UpdateDevice(l.ctx, device) + if err != nil { + l.Logger.Error("[KickOfflineByUserDeviceLogic] Update Device Error:", logger.Field("err", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update Device error: %v", err.Error()) + } + + return nil +} diff --git a/internal/logic/admin/user/updateUserAuthMethodLogic.go b/internal/logic/admin/user/updateUserAuthMethodLogic.go new file mode 100644 index 0000000..c97d466 --- /dev/null +++ b/internal/logic/admin/user/updateUserAuthMethodLogic.go @@ -0,0 +1,41 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateUserAuthMethodLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Update user auth method +func NewUpdateUserAuthMethodLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserAuthMethodLogic { + return &UpdateUserAuthMethodLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateUserAuthMethodLogic) UpdateUserAuthMethod(req *types.UpdateUserAuthMethodRequest) error { + method, err := l.svcCtx.UserModel.FindUserAuthMethodByPlatform(l.ctx, req.UserId, req.AuthType) + if err != nil { + l.Errorw("Get user auth method error", logger.Field("error", err.Error()), logger.Field("userId", req.UserId), logger.Field("authType", req.AuthType)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Get user auth method error: %v", err.Error()) + } + method.AuthType = req.AuthType + method.AuthIdentifier = req.AuthIdentifier + if err = l.svcCtx.UserModel.UpdateUserAuthMethods(l.ctx, method); err != nil { + l.Errorw("Update user auth method error", logger.Field("error", err.Error()), logger.Field("userId", req.UserId), logger.Field("authType", req.AuthType)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Update user auth method error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/user/updateUserBasicInfoLogic.go b/internal/logic/admin/user/updateUserBasicInfoLogic.go new file mode 100644 index 0000000..5f3d8a9 --- /dev/null +++ b/internal/logic/admin/user/updateUserBasicInfoLogic.go @@ -0,0 +1,52 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateUserBasicInfoLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewUpdateUserBasicInfoLogic Update user basic info +func NewUpdateUserBasicInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserBasicInfoLogic { + return &UpdateUserBasicInfoLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasiceInfoRequest) error { + userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, req.UserId) + if err != nil { + l.Errorw("[UpdateUserBasicInfoLogic] Find User Error:", logger.Field("err", err.Error()), logger.Field("userId", req.UserId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Find User Error") + } + + tool.DeepCopy(userInfo, req) + if req.Avatar != "" && !tool.IsValidImageSize(req.Avatar, 1024) { + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Invalid Image Size") + } + if req.Password != "" { + l.Infow("[UpdateUserBasicInfoLogic] Update User Password:", logger.Field("userId", req.UserId), logger.Field("password", req.Password)) + userInfo.Password = tool.EncodePassWord(req.Password) + } + + err = l.svcCtx.UserModel.Update(l.ctx, userInfo) + if err != nil { + l.Errorw("[UpdateUserBasicInfoLogic] Update User Error:", logger.Field("err", err.Error()), logger.Field("userId", req.UserId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Update User Error") + } + + return nil +} diff --git a/internal/logic/admin/user/updateUserDeviceLogic.go b/internal/logic/admin/user/updateUserDeviceLogic.go new file mode 100644 index 0000000..ecffd39 --- /dev/null +++ b/internal/logic/admin/user/updateUserDeviceLogic.go @@ -0,0 +1,40 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateUserDeviceLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// User device +func NewUpdateUserDeviceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserDeviceLogic { + return &UpdateUserDeviceLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateUserDeviceLogic) UpdateUserDevice(req *types.UserDevice) error { + device, err := l.svcCtx.UserModel.FindOneDevice(l.ctx, req.Id) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get Device error: %v", err.Error()) + } + device.Enabled = req.Enabled + err = l.svcCtx.UserModel.UpdateDevice(l.ctx, device) + if err != nil { + l.Logger.Error("[UpdateUserDeviceLogic] Update Device Error:", logger.Field("err", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update Device error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/user/updateUserNotifySettingLogic.go b/internal/logic/admin/user/updateUserNotifySettingLogic.go new file mode 100644 index 0000000..fc0142b --- /dev/null +++ b/internal/logic/admin/user/updateUserNotifySettingLogic.go @@ -0,0 +1,42 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateUserNotifySettingLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewUpdateUserNotifySettingLogic Update user notify setting +func NewUpdateUserNotifySettingLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserNotifySettingLogic { + return &UpdateUserNotifySettingLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateUserNotifySettingLogic) UpdateUserNotifySetting(req *types.UpdateUserNotifySettingRequest) error { + userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, req.UserId) + if err != nil { + l.Errorw("[UpdateUserNotifySettingLogic] Find User Error:", logger.Field("err", err.Error()), logger.Field("userId", req.UserId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Find User Error") + } + tool.DeepCopy(userInfo, req) + err = l.svcCtx.UserModel.Update(l.ctx, userInfo) + if err != nil { + l.Errorw("[UpdateUserNotifySettingLogic] Update User Error:", logger.Field("err", err.Error()), logger.Field("userId", req.UserId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Update User Error") + } + return nil +} diff --git a/internal/logic/admin/user/updateUserSubscribeLogic.go b/internal/logic/admin/user/updateUserSubscribeLogic.go new file mode 100644 index 0000000..00b7020 --- /dev/null +++ b/internal/logic/admin/user/updateUserSubscribeLogic.go @@ -0,0 +1,57 @@ +package user + +import ( + "context" + "time" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateUserSubscribeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewUpdateUserSubscribeLogic Update user subscribe +func NewUpdateUserSubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserSubscribeLogic { + return &UpdateUserSubscribeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateUserSubscribeLogic) UpdateUserSubscribe(req *types.UpdateUserSubscribeRequest) error { + userSub, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeId) + if err != nil { + l.Errorw("FindOneUserSubscribe failed:", logger.Field("error", err.Error()), logger.Field("userSubscribeId", req.UserSubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneUserSubscribe failed: %v", err.Error()) + } + err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, &user.Subscribe{ + Id: req.UserSubscribeId, + UserId: userSub.UserId, + OrderId: userSub.OrderId, + SubscribeId: req.SubscribeId, + StartTime: userSub.StartTime, + ExpireTime: time.UnixMilli(req.ExpiredAt), + Traffic: req.Traffic, + Download: req.Download, + Upload: req.Upload, + Token: userSub.Token, + UUID: userSub.UUID, + Status: userSub.Status, + }) + + if err != nil { + l.Errorw("UpdateSubscribe failed:", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateSubscribe failed: %v", err.Error()) + } + + return nil +} diff --git a/internal/logic/app/announcement/queryAnnouncementLogic.go b/internal/logic/app/announcement/queryAnnouncementLogic.go new file mode 100644 index 0000000..01c771c --- /dev/null +++ b/internal/logic/app/announcement/queryAnnouncementLogic.go @@ -0,0 +1,47 @@ +package announcement + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/announcement" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type QueryAnnouncementLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewQueryAnnouncementLogic Query announcement +func NewQueryAnnouncementLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryAnnouncementLogic { + return &QueryAnnouncementLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryAnnouncementLogic) QueryAnnouncement(req *types.QueryAnnouncementRequest) (resp *types.QueryAnnouncementResponse, err error) { + enable := true + total, list, err := l.svcCtx.AnnouncementModel.GetAnnouncementListByPage(l.ctx, req.Page, req.Size, announcement.Filter{ + Show: &enable, + Pinned: req.Pinned, + Popup: req.Popup, + }) + if err != nil { + l.Error("[QueryAnnouncementLogic] GetAnnouncementListByPage error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetAnnouncementListByPage error: %v", err.Error()) + } + resp = &types.QueryAnnouncementResponse{} + resp.Total = total + resp.List = make([]types.Announcement, 0) + tool.DeepCopy(&resp.List, list) + return +} diff --git a/internal/logic/app/auth/checkLogic.go b/internal/logic/app/auth/checkLogic.go new file mode 100644 index 0000000..3247084 --- /dev/null +++ b/internal/logic/app/auth/checkLogic.go @@ -0,0 +1,41 @@ +package auth + +import ( + "context" + + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type CheckLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Check Account +func NewCheckLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CheckLogic { + return &CheckLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CheckLogic) Check(req *types.AppAuthCheckRequest) (resp *types.AppAuthCheckResponse, err error) { + resp = &types.AppAuthCheckResponse{} + _, err = findUserByMethod(l.ctx, l.svcCtx, req.Method, req.Identifier, req.Account, req.AreaCode) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + resp.Status = false + return resp, nil + } + return resp, err + } + resp.Status = true + return +} diff --git a/internal/logic/app/auth/findUserByMethod.go b/internal/logic/app/auth/findUserByMethod.go new file mode 100644 index 0000000..b5a0b6c --- /dev/null +++ b/internal/logic/app/auth/findUserByMethod.go @@ -0,0 +1,59 @@ +package auth + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/authmethod" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/phone" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +func findUserByMethod(ctx context.Context, svcCtx *svc.ServiceContext, method, identifier, account, areaCode string) (userInfo *user.User, err error) { + var authMethods *user.AuthMethods + switch method { + case authmethod.Email: + authMethods, err = svcCtx.UserModel.FindUserAuthMethodByOpenID(ctx, authmethod.Email, account) + case authmethod.Mobile: + phoneNumber, err := phone.FormatToE164(areaCode, account) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number") + } + authMethods, err = svcCtx.UserModel.FindUserAuthMethodByOpenID(ctx, authmethod.Mobile, phoneNumber) + if err != nil { + return nil, err + } + case authmethod.Device: + userDevice, err := svcCtx.UserModel.FindOneDeviceByIdentifier(ctx, identifier) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user device imei error") + } + return svcCtx.UserModel.FindOne(ctx, userDevice.UserId) + default: + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "unknown method") + } + if err != nil { + return nil, err + } + return svcCtx.UserModel.FindOne(ctx, authMethods.UserId) +} + +func existError(method string) error { + switch method { + case authmethod.Email: + return errors.Wrapf(xerr.NewErrCode(xerr.EmailExist), "") + case authmethod.Mobile: + return errors.Wrapf(xerr.NewErrCode(xerr.TelephoneExist), "") + case authmethod.Device: + return errors.Wrapf(xerr.NewErrCode(xerr.DeviceExist), "") + default: + return errors.New("unknown method") + } +} diff --git a/internal/logic/app/auth/getAppConfigLogic.go b/internal/logic/app/auth/getAppConfigLogic.go new file mode 100644 index 0000000..d5434d3 --- /dev/null +++ b/internal/logic/app/auth/getAppConfigLogic.go @@ -0,0 +1,136 @@ +package auth + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/perfect-panel/ppanel-server/internal/model/application" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type GetAppConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// GetAppConfig +func NewGetAppConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAppConfigLogic { + return &GetAppConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAppConfigLogic) GetAppConfig(req *types.AppConfigRequest) (resp *types.AppConfigResponse, err error) { + resp = &types.AppConfigResponse{} + systems, err := l.svcCtx.SystemModel.GetSiteConfig(l.ctx) + if err != nil { + l.Errorw("[QueryApplicationConfig] GetSiteConfig error: ", logger.Field("error", err.Error())) + } + for _, sysVal := range systems { + if sysVal.Key == "CustomData" { + jsonStr := strings.ReplaceAll(sysVal.Value, "\\", "") + customData := make(map[string]interface{}) + if err = json.Unmarshal([]byte(jsonStr), &customData); err != nil { + break + } + + website := customData["website"] + if website != nil { + resp.OfficialWebsite = fmt.Sprintf("%v", website) + } + krWebsiteId := customData["kr_website_id"] + if krWebsiteId != nil { + resp.KrWebsiteId = fmt.Sprintf("%v", krWebsiteId) + } + invitationLink := customData["invitation_link"] + if krWebsiteId != nil { + resp.InvitationLink = fmt.Sprintf("%v", invitationLink) + } + + versionReview := customData["version_review"] + if versionReview != nil && req.UserAgent == "ios" { + resp.Application.VersionReview = fmt.Sprintf("%v", versionReview) + } + + contacts := customData["contacts"] + if contacts != nil { + contactsJson, err := json.Marshal(contacts) + if err == nil { + contactsMap := make(map[string]string) + err = json.Unmarshal(contactsJson, &contactsMap) + if err == nil { + resp.OfficialEmail = fmt.Sprintf("%v", contactsMap["email"]) + resp.OfficialTelegram = fmt.Sprintf("%v", contactsMap["telegram"]) + resp.OfficialTelephone = fmt.Sprintf("%v", contactsMap["telephone"]) + } + } + } + break + } + } + + var applications []*application.Application + err = l.svcCtx.ApplicationModel.Transaction(l.ctx, func(tx *gorm.DB) (err error) { + return tx.Model(applications).Preload("ApplicationVersions").Find(&applications).Error + }) + if err != nil { + l.Errorw("[QueryApplicationConfig] get application error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get application error: %v", err.Error()) + } + + if len(applications) == 0 { + return resp, nil + } + + isOk := false + for _, app := range applications { + if isOk { + break + } + resp.Application.Name = app.Name + resp.Application.Description = app.Description + applicationVersions := app.ApplicationVersions + if len(applicationVersions) != 0 { + for _, applicationVersion := range applicationVersions { + if applicationVersion.Platform == req.UserAgent { + resp.Application.Id = applicationVersion.ApplicationId + resp.Application.Url = applicationVersion.Url + resp.Application.Version = applicationVersion.Version + resp.Application.VersionDescription = applicationVersion.Description + resp.Application.IsDefault = applicationVersion.IsDefault + isOk = true + break + } + } + } + } + + configs, err := l.svcCtx.ApplicationModel.FindOneConfig(l.ctx, 1) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Logger.Error("[GetAppInfo] FindOneAppConfig error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetAppInfo FindOneAppConfig error: %v", err.Error()) + } + resp.EncryptionKey = configs.EncryptionKey + resp.EncryptionMethod = configs.EncryptionMethod + resp.Domains = strings.Split(configs.Domains, ";") + resp.StartupPicture = configs.StartupPicture + resp.StartupPictureSkipTime = configs.StartupPictureSkipTime + if configs.InvitationLink != "" { + resp.InvitationLink = configs.InvitationLink + } + if configs.KrWebsiteId != "" { + resp.KrWebsiteId = configs.KrWebsiteId + } + return +} diff --git a/internal/logic/app/auth/loginLogic.go b/internal/logic/app/auth/loginLogic.go new file mode 100644 index 0000000..e70905d --- /dev/null +++ b/internal/logic/app/auth/loginLogic.go @@ -0,0 +1,194 @@ +package auth + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/authmethod" + + "github.com/gin-gonic/gin" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/phone" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/logic/common" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/jwt" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/uuidx" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type LoginLogic struct { + logger.Logger + ctx *gin.Context + svcCtx *svc.ServiceContext +} + +// Login +func NewLoginLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *LoginLogic { + return &LoginLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *LoginLogic) Login(req *types.AppAuthRequest) (resp *types.AppAuthRespone, err error) { + + loginStatus := false + var userInfo *user.User + // Record login status + defer func(svcCtx *svc.ServiceContext) { + if userInfo != nil && userInfo.Id != 0 { + if err := svcCtx.UserModel.InsertLoginLog(l.ctx, &user.LoginLog{ + UserId: userInfo.Id, + LoginIP: l.ctx.ClientIP(), + UserAgent: l.ctx.Request.UserAgent(), + Success: &loginStatus, + }); err != nil { + l.Errorw("InsertLoginLog Error", logger.Field("error", err.Error())) + } + } + }(l.svcCtx) + + resp = &types.AppAuthRespone{} + //query user + userInfo, err = findUserByMethod(l.ctx, l.svcCtx, req.Method, req.Identifier, req.Account, req.AreaCode) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password") + } + return resp, err + } + + switch req.Method { + case authmethod.Email: + + if !l.svcCtx.Config.Email.Enable { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.EmailNotEnabled), "Email function is not enabled yet") + } + + if req.Code != "" { + cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Security.String(), req.Account) + value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() + if err != nil { + l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + + var payload common.CacheKeyPayload + err = json.Unmarshal([]byte(value), &payload) + if err != nil { + l.Errorw("Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + + if payload.Code != req.Code { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + l.svcCtx.Redis.Del(l.ctx, cacheKey) + } else { + // Verify password + if !tool.VerifyPassWord(req.Password, userInfo.Password) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password") + } + } + case authmethod.Mobile: + if !l.svcCtx.Config.Mobile.Enable { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SmsNotEnabled), "sms login is not enabled") + } + phoneNumber, err := phone.FormatToE164(req.AreaCode, req.Account) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number") + } + + if req.Code != "" { + cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.Security, phoneNumber) + value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() + if err != nil { + l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + + if value == "" { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + + var payload common.CacheKeyPayload + if err := json.Unmarshal([]byte(value), &payload); err != nil { + l.Errorw("[SendSmsCode]: Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) + } + if payload.Code != req.Code { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + l.svcCtx.Redis.Del(l.ctx, cacheKey) + } else { + // Verify password + if !tool.VerifyPassWord(req.Password, userInfo.Password) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password") + } + } + case authmethod.Device: + default: + return nil, existError(req.Method) + } + + device, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + if req.Method == authmethod.Device { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "device not exist") + } + //Add User Device + userInfo.UserDevices = append(userInfo.UserDevices, user.Device{ + UserAgent: req.UserAgent, + Identifier: req.Identifier, + Ip: l.ctx.ClientIP(), + }) + err = l.svcCtx.UserModel.Update(l.ctx, userInfo) + if err != nil { + l.Errorw("[UpdateUserBindDevice] Fail", logger.Field("error", err.Error())) + } + } + } else { + //Change the user who owns the device + if device.UserId != userInfo.Id { + device.UserId = userInfo.Id + } + device.Ip = l.ctx.ClientIP() + err = l.svcCtx.UserModel.UpdateDevice(l.ctx, device) + if err != nil { + l.Errorw("[UpdateUserBindDevice] Fail", logger.Field("error", err.Error())) + } + } + + // Generate session id + sessionId := uuidx.NewUUID().String() + // Generate token + token, err := jwt.NewJwtToken( + l.svcCtx.Config.JwtAuth.AccessSecret, + time.Now().Unix(), + l.svcCtx.Config.JwtAuth.AccessExpire, + jwt.WithOption("UserId", userInfo.Id), + jwt.WithOption("SessionId", sessionId), + ) + if err != nil { + l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) + } + sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) + if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) + } + + resp.Token = token + return +} diff --git a/internal/logic/app/auth/registerLogic.go b/internal/logic/app/auth/registerLogic.go new file mode 100644 index 0000000..cb047b3 --- /dev/null +++ b/internal/logic/app/auth/registerLogic.go @@ -0,0 +1,249 @@ +package auth + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/authmethod" + + "github.com/gin-gonic/gin" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/logic/common" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/jwt" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/phone" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/uuidx" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type CacheKeyPayload struct { + Code string `json:"code"` + LastAt int64 `json:"lastAt"` +} +type RegisterLogic struct { + logger.Logger + ctx *gin.Context + svcCtx *svc.ServiceContext +} + +// Register +func NewRegisterLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *RegisterLogic { + return &RegisterLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *RegisterLogic) Register(req *types.AppAuthRequest) (resp *types.AppAuthRespone, err error) { + resp = &types.AppAuthRespone{} + var referer *user.User + c := l.svcCtx.Config.Register + // Check if the registration is stopped + if c.StopRegister { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.StopRegister), "stop register") + } + + if req.Invite == "" { + if l.svcCtx.Config.Invite.ForcedInvite { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InviteCodeError), "invite code is required") + } + } else { + // Check if the invite code is valid + referer, err = l.svcCtx.UserModel.FindOneByReferCode(l.ctx, req.Invite) + if err != nil { + l.Errorw("FindOneByReferCode Error", logger.Field("error", err)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InviteCodeError), "invite code is invalid") + } + } + + if req.Password == "" { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.PasswordIsEmpty), "Password required") + } + + userInfo, err := findUserByMethod(l.ctx, l.svcCtx, req.Method, req.Identifier, req.Account, req.AreaCode) + if err == nil && userInfo != nil { + return nil, existError(req.Method) + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + // Generate password + pwd := tool.EncodePassWord(req.Password) + userInfo = &user.User{ + Password: pwd, + } + if referer != nil { + userInfo.RefererId = referer.Id + } + switch req.Method { + case authmethod.Email: + if !l.svcCtx.Config.Email.Enable { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.EmailNotEnabled), "Email function is not enabled yet") + } + if l.svcCtx.Config.Email.EnableVerify { + cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Register.String(), req.Account) + value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() + if err != nil { + l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + + var payload common.CacheKeyPayload + err = json.Unmarshal([]byte(value), &payload) + if err != nil { + l.Errorw("Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + + if payload.Code != req.Code { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + } + userInfo.AuthMethods = []user.AuthMethods{{ + AuthType: authmethod.Email, + AuthIdentifier: req.Account, + }} + + case authmethod.Mobile: + if req.AreaCode == "" { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneAreaCodeIsEmpty), "area code required") + } + + if !l.svcCtx.Config.Mobile.Enable { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SmsNotEnabled), "sms login is not enabled") + } + phoneNumber, err := phone.FormatToE164(req.AreaCode, req.Account) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number") + } + cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.Register, phoneNumber) + value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() + if err != nil { + l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + + var payload CacheKeyPayload + _ = json.Unmarshal([]byte(value), &payload) + if payload.Code != req.Code { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + userInfo.AuthMethods = []user.AuthMethods{{ + AuthType: authmethod.Mobile, + AuthIdentifier: phoneNumber, + Verified: true, + }} + case authmethod.Device: + oneDevice, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier) + if err == nil && oneDevice != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DeviceExist), "device exist") + } + default: + return nil, existError(req.Method) + } + + device, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + //Add User Device + userInfo.UserDevices = append(userInfo.UserDevices, user.Device{ + Ip: l.ctx.ClientIP(), + Identifier: req.Identifier, + UserAgent: req.UserAgent, + }) + } else { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error()) + } + } else { + //Delete Other User Device + err = l.svcCtx.UserModel.DeleteDevice(l.ctx, device.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "delete old user device failed: %v", err.Error()) + } else { + //User Add Device + userInfo.UserDevices = append(userInfo.UserDevices, user.Device{ + Ip: l.ctx.ClientIP(), + Identifier: req.Identifier, + UserAgent: req.UserAgent, + }) + } + } + + err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + // Save user information + if err := db.Create(userInfo).Error; err != nil { + return err + } + // Generate ReferCode + userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id) + // Update ReferCode + if err := db.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil { + return err + } + if l.svcCtx.Config.Register.EnableTrial { + // Active trial + if err = l.activeTrial(userInfo.Id); err != nil { + return err + } + } + return nil + }) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert user info failed: %v", err.Error()) + } + + // Generate session id + sessionId := uuidx.NewUUID().String() + // Generate token + token, err := jwt.NewJwtToken( + l.svcCtx.Config.JwtAuth.AccessSecret, + time.Now().Unix(), + l.svcCtx.Config.JwtAuth.AccessExpire, + jwt.WithOption("UserId", userInfo.Id), + jwt.WithOption("SessionId", sessionId), + ) + if err != nil { + l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) + } + + sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) + if err := l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) + } + + resp.Token = token + return +} + +func (l *RegisterLogic) activeTrial(uid int64) error { + sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe) + if err != nil { + return err + } + userSub := &user.Subscribe{ + Id: 0, + UserId: uid, + OrderId: 0, + SubscribeId: sub.Id, + StartTime: time.Now(), + ExpireTime: tool.AddTime(l.svcCtx.Config.Register.TrialTimeUnit, l.svcCtx.Config.Register.TrialTime, time.Now()), + Traffic: sub.Traffic, + Download: 0, + Upload: 0, + Token: uuidx.SubscribeToken(fmt.Sprintf("Trial-%v", uid)), + UUID: uuidx.NewUUID().String(), + Status: 1, + } + return l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub) +} diff --git a/internal/logic/app/auth/resetPasswordLogic.go b/internal/logic/app/auth/resetPasswordLogic.go new file mode 100644 index 0000000..21a7a5b --- /dev/null +++ b/internal/logic/app/auth/resetPasswordLogic.go @@ -0,0 +1,161 @@ +package auth + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/authmethod" + + "github.com/gin-gonic/gin" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/phone" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/logic/common" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/jwt" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/uuidx" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type ResetPasswordLogic struct { + logger.Logger + ctx *gin.Context + svcCtx *svc.ServiceContext +} + +// Reset Password +func NewResetPasswordLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *ResetPasswordLogic { + return &ResetPasswordLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ResetPasswordLogic) ResetPassword(req *types.AppAuthRequest) (resp *types.AppAuthRespone, err error) { + resp = &types.AppAuthRespone{} + userInfo, err := findUserByMethod(l.ctx, l.svcCtx, req.Method, req.Identifier, req.Account, req.AreaCode) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "query user info failed") + } + l.Errorw("FindOneByEmail Error", logger.Field("error", err)) + return nil, err + } + + switch req.Method { + case authmethod.Mobile: + if !l.svcCtx.Config.Mobile.Enable { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SmsNotEnabled), "sms login is not enabled") + } + phoneNumber, err := phone.FormatToE164(req.AreaCode, req.Account) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number") + } + cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.Security, phoneNumber) + value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() + if err != nil { + l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + + var payload common.CacheKeyPayload + err = json.Unmarshal([]byte(value), &payload) + if err != nil { + l.Errorw("Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + + if payload.Code != req.Code { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + case authmethod.Email: + if !l.svcCtx.Config.Email.Enable { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.EmailNotEnabled), "Email function is not enabled yet") + } + + cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Security.String(), req.Account) + value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() + if err != nil { + l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + + var payload CacheKeyPayload + err = json.Unmarshal([]byte(value), &payload) + if err != nil { + l.Errorw("Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + + if payload.Code != req.Code { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + default: + return nil, errors.New("unknown method") + } + + userInfo.Password = tool.EncodePassWord(req.Password) + err = l.svcCtx.UserModel.Update(l.ctx, userInfo) + if err != nil { + l.Errorw("UpdateUser Error", logger.Field("error", err)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update user info failed: %v", err.Error()) + } + + device, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + //Add User Device + userInfo.UserDevices = append(userInfo.UserDevices, user.Device{ + Ip: l.ctx.ClientIP(), + Identifier: req.Identifier, + UserAgent: req.UserAgent, + }) + } else { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error()) + } + } else { + if device.UserId != userInfo.Id { + //Change the user who owns the device + if device.UserId != userInfo.Id { + device.UserId = userInfo.Id + } + device.Ip = l.ctx.ClientIP() + err = l.svcCtx.UserModel.UpdateDevice(l.ctx, device) + if err != nil { + l.Errorw("[UpdateUserBindDevice] Fail", logger.Field("error", err.Error())) + } + } + } + + // Generate session id + sessionId := uuidx.NewUUID().String() + // Generate token + token, err := jwt.NewJwtToken( + l.svcCtx.Config.JwtAuth.AccessSecret, + time.Now().Unix(), + l.svcCtx.Config.JwtAuth.AccessExpire, + jwt.WithOption("UserId", userInfo.Id), + jwt.WithOption("SessionId", sessionId), + ) + if err != nil { + l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) + } + + sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) + if err := l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) + } + resp.Token = token + return +} diff --git a/internal/logic/app/document/queryDocumentDetailLogic.go b/internal/logic/app/document/queryDocumentDetailLogic.go new file mode 100644 index 0000000..f049042 --- /dev/null +++ b/internal/logic/app/document/queryDocumentDetailLogic.go @@ -0,0 +1,39 @@ +package document + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type QueryDocumentDetailLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewQueryDocumentDetailLogic Get document detail +func NewQueryDocumentDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryDocumentDetailLogic { + return &QueryDocumentDetailLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryDocumentDetailLogic) QueryDocumentDetail(req *types.QueryDocumentDetailRequest) (resp *types.Document, err error) { + // find document + data, err := l.svcCtx.DocumentModel.FindOne(l.ctx, req.Id) + if err != nil { + l.Error("[QueryDocumentDetailLogic] FindOne error", logger.Field("id", req.Id), logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOne error: %s", err.Error()) + } + resp = &types.Document{} + tool.DeepCopy(resp, data) + return +} diff --git a/internal/logic/app/document/queryDocumentListLogic.go b/internal/logic/app/document/queryDocumentListLogic.go new file mode 100644 index 0000000..447e7ed --- /dev/null +++ b/internal/logic/app/document/queryDocumentListLogic.go @@ -0,0 +1,48 @@ +package document + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type QueryDocumentListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get document list +func NewQueryDocumentListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryDocumentListLogic { + return &QueryDocumentListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryDocumentListLogic) QueryDocumentList() (resp *types.QueryDocumentListResponse, err error) { + total, data, err := l.svcCtx.DocumentModel.GetDocumentListByAll(l.ctx) + if err != nil { + l.Error("[QueryDocumentList] error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryDocumentList error: %v", err.Error()) + } + resp = &types.QueryDocumentListResponse{ + Total: total, + List: make([]types.Document, 0), + } + for _, item := range data { + resp.List = append(resp.List, types.Document{ + Id: item.Id, + Title: item.Title, + Tags: tool.StringMergeAndRemoveDuplicates(item.Tags), + UpdatedAt: item.UpdatedAt.UnixMilli(), + }) + } + return +} diff --git a/internal/logic/app/node/getNodeListLogic.go b/internal/logic/app/node/getNodeListLogic.go new file mode 100644 index 0000000..d37b4ed --- /dev/null +++ b/internal/logic/app/node/getNodeListLogic.go @@ -0,0 +1,91 @@ +package node + +import ( + "context" + "strconv" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/countryCenter" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type GetNodeListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Node list +func NewGetNodeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetNodeListLogic { + return &GetNodeListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetNodeListLogic) GetNodeList(req *types.AppUserSubscbribeNodeRequest) (resp *types.AppUserSubscbribeNodeResponse, err error) { + resp = &types.AppUserSubscbribeNodeResponse{List: make([]types.AppUserSubscbribeNode, 0)} + userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) + userSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscribe: %v", err.Error()) + } + + if userInfo.Id != userSubscribe.UserId { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "find user subscribe: %v", err.Error()) + } + + //拿到所有订阅下的服务组id + var ids []int64 + for _, idStr := range strings.Split(userSubscribe.Subscribe.ServerGroup, ",") { + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + continue + } + ids = append(ids, id) + } + + //根据服务组id拿到所有节点 + servers, err := l.svcCtx.ServerModel.FindServerListByGroupIds(l.ctx, ids) + if err != nil { + return nil, err + } + for _, server := range servers { + latitude, longitude, found := countryCenter.GetCountryCenterByCountryOrCity(server.Country, server.City) + if !found { + latitude = server.Latitude + longitude = server.Longitude + } + + resp.List = append(resp.List, types.AppUserSubscbribeNode{ + Id: server.Id, + Uuid: userSubscribe.UUID, + Traffic: userSubscribe.Traffic, + Upload: userSubscribe.Upload, + Download: userSubscribe.Download, + RelayNode: server.RelayNode, + RelayMode: server.RelayMode, + Longitude: server.Longitude, + Latitude: server.Latitude, + LatitudeCountry: latitude, + LongitudeCountry: longitude, + Tags: strings.Split(server.Tags, ","), + Config: server.Config, + ServerAddr: server.ServerAddr, + Protocol: server.Protocol, + SpeedLimit: server.SpeedLimit, + City: server.City, + Country: server.Country, + Name: server.Name, + }) + } + return +} diff --git a/internal/logic/app/node/getRuleGroupListLogic.go b/internal/logic/app/node/getRuleGroupListLogic.go new file mode 100644 index 0000000..70350cd --- /dev/null +++ b/internal/logic/app/node/getRuleGroupListLogic.go @@ -0,0 +1,41 @@ +package node + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetRuleGroupListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get rule group list +func NewGetRuleGroupListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetRuleGroupListLogic { + return &GetRuleGroupListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetRuleGroupListLogic) GetRuleGroupList() (resp *types.AppRuleGroupListResponse, err error) { + nodeRuleGroupList, err := l.svcCtx.ServerModel.QueryAllRuleGroup(l.ctx) + if err != nil { + l.Logger.Error("[GetRuleGroupList] get subscribe rule group list failed: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get subscribe rule group list failed: %v", err.Error()) + } + nodeRuleGroups := make([]types.ServerRuleGroup, 0) + tool.DeepCopy(&nodeRuleGroups, nodeRuleGroupList) + return &types.AppRuleGroupListResponse{ + Total: int64(len(nodeRuleGroups)), + List: nodeRuleGroups, + }, nil +} diff --git a/internal/logic/app/order/calculateCoupon.go b/internal/logic/app/order/calculateCoupon.go new file mode 100644 index 0000000..e9150cb --- /dev/null +++ b/internal/logic/app/order/calculateCoupon.go @@ -0,0 +1,13 @@ +package order + +import ( + "github.com/perfect-panel/ppanel-server/internal/model/coupon" +) + +func calculateCoupon(amount int64, couponInfo *coupon.Coupon) int64 { + if couponInfo.Type == 1 { + return int64(float64(amount) * (float64(couponInfo.Discount) / float64(100))) + } else { + return min(couponInfo.Discount, amount) + } +} diff --git a/internal/logic/app/order/calculateFee.go b/internal/logic/app/order/calculateFee.go new file mode 100644 index 0000000..432ad27 --- /dev/null +++ b/internal/logic/app/order/calculateFee.go @@ -0,0 +1,20 @@ +package order + +import "github.com/perfect-panel/ppanel-server/internal/model/payment" + +func calculateFee(amount int64, config *payment.Payment) int64 { + var fee float64 + switch config.FeeMode { + case 0: + return 0 + case 1: + fee = float64(amount) * (float64(config.FeePercent) / float64(100)) + case 2: + if amount > 0 { + fee = float64(config.FeeAmount) + } + case 3: + fee = float64(amount)*(float64(config.FeePercent)/float64(100)) + float64(config.FeeAmount) + } + return int64(fee) +} diff --git a/internal/logic/app/order/checkoutOrderLogic.go b/internal/logic/app/order/checkoutOrderLogic.go new file mode 100644 index 0000000..1852943 --- /dev/null +++ b/internal/logic/app/order/checkoutOrderLogic.go @@ -0,0 +1,373 @@ +package order + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + + order2 "github.com/perfect-panel/ppanel-server/internal/logic/public/order" + "github.com/perfect-panel/ppanel-server/pkg/payment/payssion" + + paymentPlatform "github.com/perfect-panel/ppanel-server/pkg/payment" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/model/order" + "github.com/perfect-panel/ppanel-server/internal/model/payment" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/exchangeRate" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/payment/alipay" + "github.com/perfect-panel/ppanel-server/pkg/payment/epay" + "github.com/perfect-panel/ppanel-server/pkg/payment/stripe" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + queueType "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type CheckoutOrderLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +type CurrencyConfig struct { + CurrencyUnit string + CurrencySymbol string + AccessKey string +} + +const ( + Stripe = "Stripe" + QR = "qr" + Link = "link" +) + +// NewCheckoutOrderLogic Checkout order +func NewCheckoutOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CheckoutOrderLogic { + return &CheckoutOrderLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CheckoutOrderLogic) CheckoutOrder(req *types.CheckoutOrderRequest, requestHost string) (resp *types.CheckoutOrderResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + l.Error("[CheckoutOrderLogic] Invalid access") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid access") + } + // find order + orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo) + if err != nil { + l.Error("[CheckoutOrderLogic] FindOneByOrderNo error", logger.Field("orderNo", req.OrderNo), logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneByOrderNo error: %s", err.Error()) + } + + if orderInfo.Status != 1 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Order status error") + } + + paymentConfig, err := l.svcCtx.PaymentModel.FindOne(l.ctx, orderInfo.PaymentId) + if err != nil { + l.Error("[CheckoutOrderLogic] FindOneByPaymentMark error", logger.Field("paymentMark", orderInfo.Method), logger.Field("PaymentID", orderInfo.PaymentId), logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneByPaymentMark error: %s", err.Error()) + } + var stripePayment *types.StripePayment = nil + var url, t string + + // switch payment method + switch paymentPlatform.ParsePlatform(paymentConfig.Platform) { + case paymentPlatform.Stripe: + result, err := l.stripePayment(paymentConfig.Config, orderInfo, u) + if err != nil { + l.Error("[CheckoutOrderLogic] stripePayment error", logger.Field("error", err.Error())) + return nil, err + } + stripePayment = result + t = Stripe + case paymentPlatform.EPay: + // epay + url, err = l.epayPayment(paymentConfig, orderInfo, req.ReturnUrl, requestHost) + if err != nil { + l.Error("[CheckoutOrderLogic] epayPayment error", logger.Field("error", err.Error())) + return nil, err + } + t = Link + case paymentPlatform.AlipayF2F: + // alipay f2f + url, err = l.alipayF2fPayment(paymentConfig, orderInfo, requestHost) + if err != nil { + return nil, err + } + t = QR + case paymentPlatform.Payssion: + logger.Infof("匹配配置类型: %v", order2.Payssion) + url, err = l.payssionPayment(paymentConfig, orderInfo, requestHost) + if err != nil { + l.Error("[CheckoutOrderLogic] epayPayment error", logger.Field("error", err.Error())) + return nil, err + } + t = Link + case paymentPlatform.Balance: + // balance + if err = l.balancePayment(u, orderInfo); err != nil { + return nil, err + } + t = paymentPlatform.Balance.String() + default: + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Payment method not supported") + } + return &types.CheckoutOrderResponse{ + Type: t, + CheckoutUrl: url, + Stripe: stripePayment, + }, nil +} + +// Query exchange rate +func (l *CheckoutOrderLogic) queryExchangeRate(to string, src int64) (amount float64, err error) { + amount = float64(src) / float64(100) + // query system currency + currency, err := l.svcCtx.SystemModel.GetCurrencyConfig(l.ctx) + if err != nil { + l.Error("[CheckoutOrderLogic] GetCurrencyConfig error", logger.Field("error", err.Error())) + return 0, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetCurrencyConfig error: %s", err.Error()) + } + configs := &CurrencyConfig{} + tool.SystemConfigSliceReflectToStruct(currency, configs) + if configs.AccessKey == "" { + return amount, nil + } + if configs.CurrencyUnit != to { + // query exchange rate + result, err := exchangeRate.GetExchangeRete(configs.CurrencyUnit, to, configs.AccessKey, 1) + if err != nil { + return 0, err + } + amount = result * amount + } + return amount, nil +} + +// Stripe Payment +func (l *CheckoutOrderLogic) stripePayment(config string, info *order.Order, u *user.User) (*types.StripePayment, error) { + // stripe WeChat pay or stripe alipay + stripeConfig := payment.StripeConfig{} + if err := json.Unmarshal([]byte(config), &stripeConfig); err != nil { + l.Error("[CheckoutOrderLogic] Unmarshal error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) + } + client := stripe.NewClient(stripe.Config{ + SecretKey: stripeConfig.SecretKey, + PublicKey: stripeConfig.PublicKey, + WebhookSecret: stripeConfig.WebhookSecret, + }) + // Calculate the amount with exchange rate + amount, err := l.queryExchangeRate("CNY", info.Amount) + if err != nil { + l.Error("[CheckoutOrderLogic] queryExchangeRate error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "queryExchangeRate error: %s", err.Error()) + } + convertAmount := int64(amount * 100) + // create payment + result, err := client.CreatePaymentSheet(&stripe.Order{ + OrderNo: info.OrderNo, + Subscribe: strconv.FormatInt(info.SubscribeId, 10), + Amount: convertAmount, + Currency: "cny", + Payment: stripeConfig.Payment, + }, + &stripe.User{ + UserId: u.Id, + }) + if err != nil { + l.Error("[CheckoutOrderLogic] CreatePaymentSheet error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "CreatePaymentSheet error: %s", err.Error()) + } + tradeNo := result.TradeNo + stripePayment := &types.StripePayment{ + PublishableKey: stripeConfig.PublicKey, + ClientSecret: result.ClientSecret, + Method: stripeConfig.Payment, + } + // save payment + info.TradeNo = tradeNo + err = l.svcCtx.OrderModel.Update(l.ctx, info) + if err != nil { + l.Error("[CheckoutOrderLogic] Update error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Update error: %s", err.Error()) + } + return stripePayment, nil +} + +// epay payment +func (l *CheckoutOrderLogic) epayPayment(config *payment.Payment, info *order.Order, returnUrl, requestHost string) (string, error) { + epayConfig := payment.EPayConfig{} + if err := json.Unmarshal([]byte(config.Config), &epayConfig); err != nil { + l.Errorw("[CheckoutOrderLogic] Unmarshal error", logger.Field("error", err.Error())) + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) + } + client := epay.NewClient(epayConfig.Pid, epayConfig.Url, epayConfig.Key) + // Calculate the amount with exchange rate + amount, err := l.queryExchangeRate("CNY", info.Amount) + if err != nil { + return "", err + } + notifyUrl := "" + if config.Domain != "" { + notifyUrl = config.Domain + "/v1/notify/" + config.Platform + "/" + config.Token + } else { + host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string) + if !ok { + host = l.svcCtx.Config.Host + } + notifyUrl = "https://" + host + "/v1/notify/" + config.Platform + "/" + config.Token + } + // create payment + url := client.CreatePayUrl(epay.Order{ + Name: l.svcCtx.Config.Site.SiteName, + Amount: amount, + OrderNo: info.OrderNo, + SignType: "MD5", + NotifyUrl: notifyUrl, + ReturnUrl: returnUrl, + }) + return url, nil +} + +// alipay f2f payment +func (l *CheckoutOrderLogic) alipayF2fPayment(pay *payment.Payment, info *order.Order, requestHost string) (string, error) { + f2FConfig := payment.AlipayF2FConfig{} + if err := json.Unmarshal([]byte(pay.Config), &f2FConfig); err != nil { + l.Error("[CheckoutOrderLogic] Unmarshal error", logger.Field("error", err.Error())) + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) + } + var domain string + if pay.Domain != "" { + domain = pay.Domain + } else { + domain = fmt.Sprintf("http://%s", requestHost) + } + client := alipay.NewClient(alipay.Config{ + AppId: f2FConfig.AppId, + PrivateKey: f2FConfig.PrivateKey, + PublicKey: f2FConfig.PublicKey, + InvoiceName: f2FConfig.InvoiceName, + NotifyURL: domain + "/notify/alipay", + }) + // Calculate the amount with exchange rate + amount, err := l.queryExchangeRate("CNY", info.Amount) + if err != nil { + l.Error("[CheckoutOrderLogic] queryExchangeRate error", logger.Field("error", err.Error())) + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "queryExchangeRate error: %s", err.Error()) + } + convertAmount := int64(amount * 100) + // create payment + QRCode, err := client.PreCreateTrade(l.ctx, alipay.Order{ + OrderNo: info.OrderNo, + Amount: convertAmount, + }) + if err != nil { + l.Error("[CheckoutOrderLogic] PreCreateTrade error", logger.Field("error", err.Error())) + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "PreCreateTrade error: %s", err.Error()) + } + return QRCode, nil +} + +// Balance payment +func (l *CheckoutOrderLogic) balancePayment(u *user.User, o *order.Order) error { + var userInfo user.User + err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + err := db.Model(&user.User{}).Where("id = ?", u.Id).First(&userInfo).Error + if err != nil { + return err + } + + if userInfo.Balance < o.Amount { + return errors.Wrapf(xerr.NewErrCode(xerr.InsufficientBalance), "Insufficient balance") + } + // deduct balance + userInfo.Balance -= o.Amount + err = l.svcCtx.UserModel.Update(l.ctx, &userInfo) + if err != nil { + return err + } + // create balance log + balanceLog := &user.BalanceLog{ + Id: 0, + UserId: u.Id, + Amount: o.Amount, + Type: 3, + OrderId: o.Id, + Balance: userInfo.Balance, + } + err = db.Create(balanceLog).Error + if err != nil { + return err + } + return l.svcCtx.OrderModel.UpdateOrderStatus(l.ctx, o.OrderNo, 2) + }) + if err != nil { + l.Error("[CheckoutOrderLogic] Transaction error", logger.Field("error", err.Error()), logger.Field("orderNo", o.OrderNo)) + return err + } + // create activity order task + payload := queueType.ForthwithActivateOrderPayload{ + OrderNo: o.OrderNo, + } + bytes, err := json.Marshal(payload) + if err != nil { + l.Error("[CheckoutOrderLogic] Marshal error", logger.Field("error", err.Error())) + return err + } + + task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes) + _, err = l.svcCtx.Queue.EnqueueContext(l.ctx, task) + if err != nil { + l.Error("[CheckoutOrderLogic] Enqueue error", logger.Field("error", err.Error())) + return err + } + l.Logger.Info("[CheckoutOrderLogic] Enqueue success", logger.Field("orderNo", o.OrderNo)) + return nil +} + +func (l *CheckoutOrderLogic) payssionPayment(config *payment.Payment, info *order.Order, returnUrl string) (string, error) { + payssionConfig := payment.PayssionConfig{} + if err := json.Unmarshal([]byte(config.Config), &payssionConfig); err != nil { + l.Errorw("[CheckoutOrderLogic] Unmarshal error", logger.Field("error", err.Error())) + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) + } + client := payssion.NewClient(payssionConfig.ApiKey, payssionConfig.SecretKey, payssionConfig.PmId, payssionConfig.Currency, payssionConfig.QueryUrl, payssionConfig.CreateUrl) + // Calculate the amount with exchange rate + amount, err := l.queryExchangeRate("CNY", info.Amount) + if err != nil { + return "", err + } + notifyUrl := "" + if config.Domain != "" { + notifyUrl = config.Domain + "/v1/notify/" + config.Platform + "/" + config.Token + } else { + host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string) + if !ok { + host = l.svcCtx.Config.Host + } + notifyUrl = "https://" + host + "/v1/notify/" + config.Platform + "/" + config.Token + } + // create payment + url, err := client.CreateOrder(payssion.Order{ + Name: l.svcCtx.Config.Site.SiteName, + Amount: amount, + OrderNo: info.OrderNo, + NotifyUrl: notifyUrl, + ReturnUrl: returnUrl, + }) + return url, err +} diff --git a/internal/logic/app/order/closeOrderLogic.go b/internal/logic/app/order/closeOrderLogic.go new file mode 100644 index 0000000..b98abc9 --- /dev/null +++ b/internal/logic/app/order/closeOrderLogic.go @@ -0,0 +1,205 @@ +package order + +import ( + "context" + "encoding/json" + "github.com/perfect-panel/ppanel-server/pkg/payment/payssion" + + paymentPlatform "github.com/perfect-panel/ppanel-server/pkg/payment" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/payment/stripe" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/model/order" + "github.com/perfect-panel/ppanel-server/internal/model/payment" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/payment/alipay" +) + +type CloseOrderLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewCloseOrderLogic Close order +func NewCloseOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CloseOrderLogic { + return &CloseOrderLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CloseOrderLogic) CloseOrder(req *types.CloseOrderRequest) error { + // Find order information by order number + orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo) + if err != nil { + l.Error("[CloseOrder] Find order info failed", + logger.Field("error", err.Error()), + logger.Field("orderNo", req.OrderNo), + ) + return nil + } + // If the order status is not 1, it means that the order has been closed or paid + if orderInfo.Status != 1 { + l.Info("[CloseOrder] Order status is not 1", + logger.Field("orderNo", req.OrderNo), + logger.Field("status", orderInfo.Status), + ) + return nil + } + + err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { + // update order status + err := tx.Model(&order.Order{}).Where("order_no = ?", req.OrderNo).Update("status", 3).Error + if err != nil { + l.Error("[CloseOrder] Update order status failed", + logger.Field("error", err.Error()), + logger.Field("orderNo", req.OrderNo), + ) + return err + } + // refund deduction amount to user deduction balance + if orderInfo.GiftAmount > 0 { + userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, orderInfo.UserId) + if err != nil { + l.Error("[CloseOrder] Find user info failed", + logger.Field("error", err.Error()), + logger.Field("user_id", orderInfo.UserId), + ) + return err + } + deduction := userInfo.GiftAmount + orderInfo.GiftAmount + err = tx.Model(&user.User{}).Where("id = ?", orderInfo.UserId).Update("deduction", deduction).Error + if err != nil { + l.Error("[CloseOrder] Refund deduction amount failed", + logger.Field("error", err.Error()), + logger.Field("uid", orderInfo.UserId), + logger.Field("deduction", orderInfo.GiftAmount), + ) + return err + } + // Record the deduction refund log + giftAmountLog := &user.GiftAmountLog{ + UserId: orderInfo.UserId, + OrderNo: orderInfo.OrderNo, + Amount: orderInfo.GiftAmount, + Type: 1, + Balance: deduction, + Remark: "Order cancellation refund", + } + err = tx.Model(&user.GiftAmountLog{}).Create(giftAmountLog).Error + if err != nil { + l.Error("[CloseOrder] Record cancellation refund log failed", + logger.Field("error", err.Error()), + logger.Field("uid", orderInfo.UserId), + logger.Field("deduction", orderInfo.GiftAmount), + ) + return err + } + // update user cache + return l.svcCtx.UserModel.UpdateUserCache(l.ctx, userInfo) + } + return nil + }) + if err != nil { + return err + } + return nil +} + +// confirmationPayment Determine whether the payment is successful +// +//nolint:unused +func (l *CloseOrderLogic) confirmationPayment(order *order.Order) bool { + paymentConfig, err := l.svcCtx.PaymentModel.FindOne(l.ctx, order.PaymentId) + if err != nil { + l.Error("[CloseOrder] Find payment config failed", logger.Field("error", err.Error()), logger.Field("paymentMark", order.Method)) + return false + } + switch paymentPlatform.ParsePlatform(order.Method) { + case paymentPlatform.AlipayF2F: + if l.queryAlipay(paymentConfig, order.TradeNo) { + return true + } + case paymentPlatform.Payssion: + if l.queryPayssion(paymentConfig, order.TradeNo) { + return true + } + case paymentPlatform.Stripe: + if l.queryStripe(paymentConfig, order.TradeNo) { + return true + } + default: + l.Info("[CloseOrder] Unsupported payment method", logger.Field("paymentMethod", order.Method)) + } + return false +} + +// queryAlipay Query Alipay payment status +func (l *CloseOrderLogic) queryAlipay(paymentConfig *payment.Payment, TradeNo string) bool { + config := payment.AlipayF2FConfig{} + if err := json.Unmarshal([]byte(paymentConfig.Config), &config); err != nil { + l.Error("[CloseOrder] Unmarshal payment config failed", logger.Field("error", err.Error()), logger.Field("config", paymentConfig.Config)) + return false + } + client := alipay.NewClient(alipay.Config{ + AppId: config.AppId, + PrivateKey: config.PrivateKey, + PublicKey: config.PublicKey, + InvoiceName: config.InvoiceName, + }) + status, err := client.QueryTrade(l.ctx, TradeNo) + if err != nil { + l.Error("[CloseOrder] Query trade failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo)) + return false + } + if status == alipay.Success || status == alipay.Finished { + return true + } + return false +} + +// queryStripe Query Stripe payment status +func (l *CloseOrderLogic) queryStripe(paymentConfig *payment.Payment, TradeNo string) bool { + config := payment.StripeConfig{} + if err := json.Unmarshal([]byte(paymentConfig.Config), &config); err != nil { + l.Error("[CloseOrder] Unmarshal payment config failed", logger.Field("error", err.Error()), logger.Field("config", paymentConfig.Config)) + return false + } + client := stripe.NewClient(stripe.Config{ + PublicKey: config.PublicKey, + SecretKey: config.SecretKey, + WebhookSecret: config.WebhookSecret, + }) + status, err := client.QueryOrderStatus(TradeNo) + if err != nil { + l.Error("[CloseOrder] Query order status failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo)) + return false + } + return status +} + +//nolint:unused +func (l *CloseOrderLogic) queryPayssion(paymentConfig *payment.Payment, TradeNo string) bool { + l.Info("[CloseOrder]1 Query Payssion", logger.Field("TradeNo", TradeNo)) + payssionConfig := payment.PayssionConfig{} + if err := json.Unmarshal([]byte(paymentConfig.Config), &payssionConfig); err != nil { + l.Errorw("[CloseOrder] Unmarshal error", logger.Field("error", err.Error())) + return false + } + client := payssion.NewClient(payssionConfig.ApiKey, payssionConfig.SecretKey, payssionConfig.PmId, payssionConfig.Currency, payssionConfig.QueryUrl, payssionConfig.CreateUrl) + l.Infof("[CloseOrder]2 Query Payssion", logger.Field("TradeNo", TradeNo)) + // create payment + result, err := client.QueryOrder(TradeNo) + if err != nil { + l.Errorw("[CloseOrder] Query order status failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo)) + return false + } + l.Infof("[CloseOrder]3 Query Payssion", logger.Field("TradeNo", TradeNo)) + return result.Transaction.State == "completed" +} diff --git a/internal/logic/app/order/getDiscount.go b/internal/logic/app/order/getDiscount.go new file mode 100644 index 0000000..4d896f9 --- /dev/null +++ b/internal/logic/app/order/getDiscount.go @@ -0,0 +1,14 @@ +package order + +import "github.com/perfect-panel/ppanel-server/internal/types" + +func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 { + var finalDiscount int64 = 100 + + for _, discount := range discounts { + if inputMonths >= discount.Quantity && discount.Discount < finalDiscount { + finalDiscount = discount.Discount + } + } + return float64(finalDiscount) / float64(100) +} diff --git a/internal/logic/app/order/preCreateOrderLogic.go b/internal/logic/app/order/preCreateOrderLogic.go new file mode 100644 index 0000000..d36cd10 --- /dev/null +++ b/internal/logic/app/order/preCreateOrderLogic.go @@ -0,0 +1,104 @@ +package order + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type PreCreateOrderLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Pre create order +func NewPreCreateOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PreCreateOrderLogic { + return &PreCreateOrderLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (resp *types.PreOrderResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + // find subscribe plan + sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribeId) + if err != nil { + l.Error("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", req.SubscribeId)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error()) + } + var discount float64 = 1 + if sub.Discount != "" { + var dis []types.SubscribeDiscount + _ = json.Unmarshal([]byte(sub.Discount), &dis) + discount = getDiscount(dis, req.Quantity) + } + price := sub.UnitPrice * req.Quantity + amount := int64(float64(price) * discount) + discountAmount := price - amount + var coupon int64 + if req.Coupon != "" { + couponInfo, err := l.svcCtx.CouponModel.FindOneByCode(l.ctx, req.Coupon) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotExist), "coupon not found") + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error()) + } + if couponInfo.Count <= couponInfo.UsedCount { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponUsed), "coupon used") + } + coupon = calculateCoupon(amount, couponInfo) + } + amount -= coupon + + var deductionAmount int64 + // Check user deduction amount + if u.GiftAmount > 0 { + if u.GiftAmount >= amount { + deductionAmount = amount + amount = 0 + } else { + deductionAmount = u.GiftAmount + amount -= u.GiftAmount + } + } + + payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) + if err != nil { + l.Logger.Error("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error()) + } + var feeAmount int64 + // Calculate the handling fee + if amount > 0 { + feeAmount = calculateFee(amount, payment) + } + amount += feeAmount + + resp = &types.PreOrderResponse{ + Price: price, + Amount: amount, + Discount: discountAmount, + GiftAmount: deductionAmount, + Coupon: req.Coupon, + CouponDiscount: coupon, + FeeAmount: feeAmount, + } + return +} diff --git a/internal/logic/app/order/purchaseLogic.go b/internal/logic/app/order/purchaseLogic.go new file mode 100644 index 0000000..7934cc6 --- /dev/null +++ b/internal/logic/app/order/purchaseLogic.go @@ -0,0 +1,204 @@ +package order + +import ( + "context" + "encoding/json" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/model/order" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + queue "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type PurchaseLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +const CloseOrderTimeMinutes = 15 + +// purchase Subscription +func NewPurchaseLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PurchaseLogic { + return &PurchaseLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.PurchaseOrderResponse, err error) { + + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + // find user subscription + + if l.svcCtx.Config.Subscribe.SingleModel { + userSubs, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id) + if err != nil { + l.Error("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscription error: %v", err.Error()) + } + if (len(userSubs) == 1 && //订阅数等于1 + //启用试用 + l.svcCtx.Config.Register.EnableTrial && + //试用订阅ID不等于0 + l.svcCtx.Config.Register.TrialSubscribe != 0 && + //使用者订阅ID不等于试用订阅ID + userSubs[0].SubscribeId != l.svcCtx.Config.Register.TrialSubscribe) || len(userSubs) > 1 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserSubscribeExist), "user has subscription") + } + } + + // find subscribe plan + sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribeId) + + if err != nil { + l.Error("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", req.SubscribeId)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error()) + } + // check subscribe plan status + if !*sub.Sell { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "subscribe not sell") + } + var discount float64 = 1 + if sub.Discount != "" { + var dis []types.SubscribeDiscount + _ = json.Unmarshal([]byte(sub.Discount), &dis) + discount = getDiscount(dis, req.Quantity) + } + price := sub.UnitPrice * req.Quantity + // discount amount + amount := int64(float64(price) * discount) + discountAmount := price - amount + var coupon int64 = 0 + // Calculate the coupon deduction + if req.Coupon != "" { + couponInfo, err := l.svcCtx.CouponModel.FindOneByCode(l.ctx, req.Coupon) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotExist), "coupon not found") + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error()) + } + if couponInfo.Count <= couponInfo.UsedCount { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponUsed), "coupon used") + } + coupon = calculateCoupon(amount, couponInfo) + } + // Calculate the handling fee + amount -= coupon + var deductionAmount int64 + // Check user deduction amount + if u.GiftAmount > 0 { + if u.GiftAmount >= amount { + deductionAmount = amount + amount = 0 + u.GiftAmount -= amount + } else { + deductionAmount = u.GiftAmount + amount -= u.GiftAmount + u.GiftAmount = 0 + } + } + // find payment method + payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) + if err != nil { + l.Logger.Error("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error()) + } + var feeAmount int64 + // Calculate the handling fee + if amount > 0 { + feeAmount = calculateFee(amount, payment) + } + // query user is new purchase or renewal + isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id) + if err != nil { + l.Error("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user order error: %v", err.Error()) + } + // create order + orderInfo := &order.Order{ + UserId: u.Id, + OrderNo: tool.GenerateTradeNo(), + Type: 1, + Quantity: req.Quantity, + Price: price, + Amount: amount, + Discount: discountAmount, + GiftAmount: deductionAmount, + Coupon: req.Coupon, + CouponDiscount: coupon, + PaymentId: req.Payment, + Method: payment.Platform, + FeeAmount: feeAmount, + Status: 1, + IsNew: isNew, + SubscribeId: req.SubscribeId, + } + // Database transaction + err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error { + // update user deduction && Pre deduction ,Return after canceling the order + if orderInfo.GiftAmount > 0 { + // update user deduction && Pre deduction ,Return after canceling the order + if e := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil { + l.Error("[Purchase] Database update error", logger.Field("error", err.Error()), logger.Field("user", u)) + return e + } + // create deduction record + giftAmountLog := user.GiftAmountLog{ + UserId: orderInfo.UserId, + OrderNo: orderInfo.OrderNo, + Amount: orderInfo.GiftAmount, + Type: 2, + Balance: u.GiftAmount, + Remark: "Purchase order deduction", + } + if e := db.Model(&user.GiftAmountLog{}).Create(&giftAmountLog).Error; e != nil { + l.Error("[Purchase] Database insert error", + logger.Field("error", err.Error()), + logger.Field("deductionLog", giftAmountLog), + ) + return e + } + } + // insert order + return db.Model(&order.Order{}).Create(&orderInfo).Error + }) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert order error: %v", err.Error()) + } + // Deferred task + payload := queue.DeferCloseOrderPayload{ + OrderNo: orderInfo.OrderNo, + } + val, err := json.Marshal(payload) + if err != nil { + l.Error("[CreateOrder] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload)) + } + task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3)) + taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute)) + if err != nil { + l.Error("[CreateOrder] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task)) + } else { + l.Info("[CreateOrder] Enqueue task success", logger.Field("TaskID", taskInfo.ID)) + } + + return &types.PurchaseOrderResponse{ + OrderNo: orderInfo.OrderNo, + }, nil +} diff --git a/internal/logic/app/order/queryOrderDetailLogic.go b/internal/logic/app/order/queryOrderDetailLogic.go new file mode 100644 index 0000000..5cc6b55 --- /dev/null +++ b/internal/logic/app/order/queryOrderDetailLogic.go @@ -0,0 +1,40 @@ +package order + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type QueryOrderDetailLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get order +func NewQueryOrderDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryOrderDetailLogic { + return &QueryOrderDetailLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryOrderDetailLogic) QueryOrderDetail(req *types.QueryOrderDetailRequest) (resp *types.OrderDetail, err error) { + orderInfo, err := l.svcCtx.OrderModel.FindOneDetailsByOrderNo(l.ctx, req.OrderNo) + if err != nil { + l.Error("[QueryOrderDetail] Database query error", logger.Field("error", err.Error()), logger.Field("order_no", req.OrderNo)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find order error: %v", err.Error()) + } + resp = &types.OrderDetail{} + tool.DeepCopy(resp, orderInfo) + // Prevent commission amount leakage + resp.Commission = 0 + return +} diff --git a/internal/logic/app/order/queryOrderListLogic.go b/internal/logic/app/order/queryOrderListLogic.go new file mode 100644 index 0000000..245e5d6 --- /dev/null +++ b/internal/logic/app/order/queryOrderListLogic.go @@ -0,0 +1,56 @@ +package order + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type QueryOrderListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get order list +func NewQueryOrderListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryOrderListLogic { + return &QueryOrderListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryOrderListLogic) QueryOrderList(req *types.QueryOrderListRequest) (resp *types.QueryOrderListResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + total, data, err := l.svcCtx.OrderModel.QueryOrderListByPage(l.ctx, req.Page, req.Size, 0, u.Id, 0, "") + if err != nil { + l.Error("[QueryOrderListLogic] Query order list failed", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query order list failed") + } + resp = &types.QueryOrderListResponse{ + Total: total, + List: make([]types.OrderDetail, 0), + } + for _, item := range data { + var orderInfo types.OrderDetail + tool.DeepCopy(&orderInfo, item) + // Prevent commission amount leakage + orderInfo.Commission = 0 + resp.List = append(resp.List, orderInfo) + } + + return +} diff --git a/internal/logic/app/order/rechargeLogic.go b/internal/logic/app/order/rechargeLogic.go new file mode 100644 index 0000000..2957c54 --- /dev/null +++ b/internal/logic/app/order/rechargeLogic.go @@ -0,0 +1,92 @@ +package order + +import ( + "context" + "encoding/json" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/model/order" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + queue "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/pkg/errors" +) + +type RechargeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewRechargeLogic Recharge +func NewRechargeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RechargeLogic { + return &RechargeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *RechargeLogic) Recharge(req *types.RechargeOrderRequest) (resp *types.RechargeOrderResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + // find payment method + payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) + if err != nil { + l.Error("[Recharge] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) + return nil, errors.Wrapf(err, "find payment error: %v", err.Error()) + } + // Calculate the handling fee + feeAmount := calculateFee(req.Amount, payment) + // query user is new purchase or renewal + isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id) + if err != nil { + l.Error("[Recharge] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id)) + return nil, errors.Wrapf(err, "query user error: %v", err.Error()) + } + orderInfo := order.Order{ + UserId: u.Id, + OrderNo: tool.GenerateTradeNo(), + Type: 4, + Price: req.Amount, + Amount: req.Amount + feeAmount, + FeeAmount: feeAmount, + PaymentId: req.Payment, + Method: payment.Platform, + Status: 1, + IsNew: isNew, + } + err = l.svcCtx.OrderModel.Insert(l.ctx, &orderInfo) + if err != nil { + l.Error("[Recharge] Database insert error", logger.Field("error", err.Error()), logger.Field("order", orderInfo)) + return nil, errors.Wrapf(err, "insert order error: %v", err.Error()) + } + // Deferred task + payload := queue.DeferCloseOrderPayload{ + OrderNo: orderInfo.OrderNo, + } + val, err := json.Marshal(payload) + if err != nil { + l.Error("[Recharge] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload)) + } + task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3)) + taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute)) + if err != nil { + l.Error("[Recharge] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task)) + } else { + l.Info("[Recharge] Enqueue task success", logger.Field("TaskID", taskInfo.ID)) + } + return &types.RechargeOrderResponse{ + OrderNo: orderInfo.OrderNo, + }, nil +} diff --git a/internal/logic/app/order/renewalLogic.go b/internal/logic/app/order/renewalLogic.go new file mode 100644 index 0000000..4878160 --- /dev/null +++ b/internal/logic/app/order/renewalLogic.go @@ -0,0 +1,178 @@ +package order + +import ( + "context" + "encoding/json" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/model/order" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + queue "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type RenewalLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Renewal Subscription +func NewRenewalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RenewalLogic { + return &RenewalLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.RenewalOrderResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + orderNo := tool.GenerateTradeNo() + // find user subscribe + userSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeID) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscribe error: %v", err.Error()) + } + // find subscription + sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, userSubscribe.SubscribeId) + if err != nil { + l.Error("[Renewal] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", userSubscribe.SubscribeId)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error()) + } + // check subscribe plan status + if !*sub.Sell { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "subscribe not sell") + } + var discount float64 = 1 + if sub.Discount != "" { + var dis []types.SubscribeDiscount + _ = json.Unmarshal([]byte(sub.Discount), &dis) + discount = getDiscount(dis, req.Quantity) + } + price := sub.UnitPrice * req.Quantity + amount := int64(float64(price) * discount) + discountAmount := price - amount + var coupon int64 = 0 + if req.Coupon != "" { + couponInfo, err := l.svcCtx.CouponModel.FindOneByCode(l.ctx, req.Coupon) + if err != nil { + l.Error("[Renewal] Database query error", logger.Field("error", err.Error()), logger.Field("coupon", req.Coupon)) + return nil, errors.Wrapf(err, "find coupon error: %v", err.Error()) + } + if couponInfo.Count <= couponInfo.UsedCount { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponUsed), "coupon used") + } + coupon = calculateCoupon(amount, couponInfo) + } + payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) + if err != nil { + l.Error("[Renewal] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) + return nil, errors.Wrapf(err, "find payment error: %v", err.Error()) + } + amount -= coupon + + var deductionAmount int64 + // Check user deduction amount + if u.GiftAmount > 0 { + if u.GiftAmount >= amount { + deductionAmount = amount + amount = 0 + u.GiftAmount -= amount + } else { + deductionAmount = u.GiftAmount + amount -= u.GiftAmount + u.GiftAmount = 0 + } + } + + var feeAmount int64 + // Calculate the handling fee + if amount > 0 { + feeAmount = calculateFee(amount, payment) + } + + amount += feeAmount + + // create order + orderInfo := order.Order{ + UserId: u.Id, + ParentId: userSubscribe.OrderId, + OrderNo: orderNo, + Type: 2, + Quantity: req.Quantity, + Price: price, + Amount: amount, + GiftAmount: deductionAmount, + Discount: discountAmount, + Coupon: req.Coupon, + CouponDiscount: coupon, + PaymentId: payment.Id, + Method: payment.Platform, + FeeAmount: feeAmount, + Status: 1, + SubscribeId: userSubscribe.SubscribeId, + SubscribeToken: userSubscribe.Token, + } + // Database transaction + err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error { + // update user deduction && Pre deduction ,Return after canceling the order + if orderInfo.GiftAmount > 0 { + // update user deduction && Pre deduction ,Return after canceling the order + if err := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil { + l.Error("[Purchase] Database update error", logger.Field("error", err.Error()), logger.Field("user", u)) + return err + } + // create deduction record + deductionLog := user.GiftAmountLog{ + UserId: orderInfo.UserId, + OrderNo: orderInfo.OrderNo, + Amount: orderInfo.GiftAmount, + Type: 2, + Balance: u.GiftAmount, + Remark: "Renewal order deduction", + } + if err := db.Model(&user.GiftAmountLog{}).Create(&deductionLog).Error; err != nil { + l.Error("[Renewal] Database insert error", logger.Field("error", err.Error()), logger.Field("deductionLog", deductionLog)) + return err + } + } + // insert order + return db.Model(&order.Order{}).Create(&orderInfo).Error + }) + if err != nil { + l.Error("[Renewal] Database insert error", logger.Field("error", err.Error()), logger.Field("order", orderInfo)) + return nil, errors.Wrapf(err, "insert order error: %v", err.Error()) + } + // Deferred task + payload := queue.DeferCloseOrderPayload{ + OrderNo: orderInfo.OrderNo, + } + val, err := json.Marshal(payload) + if err != nil { + l.Error("[Renewal] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload)) + } + task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3)) + taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute)) + if err != nil { + l.Error("[Renewal] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task)) + } else { + l.Info("[Renewal] Enqueue task success", logger.Field("TaskID", taskInfo.ID)) + } + return &types.RenewalOrderResponse{ + OrderNo: orderInfo.OrderNo, + }, nil +} diff --git a/internal/logic/app/order/resetTrafficLogic.go b/internal/logic/app/order/resetTrafficLogic.go new file mode 100644 index 0000000..1ec2f22 --- /dev/null +++ b/internal/logic/app/order/resetTrafficLogic.go @@ -0,0 +1,146 @@ +package order + +import ( + "context" + "encoding/json" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + + "gorm.io/gorm" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/model/order" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/tool" + queue "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/pkg/errors" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type ResetTrafficLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Reset traffic +func NewResetTrafficLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetTrafficLogic { + return &ResetTrafficLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ResetTrafficLogic) ResetTraffic(req *types.ResetTrafficOrderRequest) (resp *types.ResetTrafficOrderResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + // find user subscription + userSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeID) + if err != nil { + l.Error("[ResetTraffic] Database query error", logger.Field("error", err.Error()), logger.Field("UserSubscribeID", req.UserSubscribeID)) + return nil, errors.Wrapf(err, "find user subscribe error: %v", err.Error()) + } + if userSubscribe.Subscribe == nil { + l.Error("[ResetTraffic] subscribe not found", logger.Field("UserSubscribeID", req.UserSubscribeID)) + return nil, errors.New("subscribe not found") + } + amount := userSubscribe.Subscribe.Replacement + var deductionAmount int64 + // Check user deduction amount + if u.GiftAmount > 0 { + if u.GiftAmount >= amount { + deductionAmount = amount + amount = 0 + u.GiftAmount -= amount + } else { + deductionAmount = u.GiftAmount + amount -= u.GiftAmount + u.GiftAmount = 0 + } + } + // find payment method + payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) + if err != nil { + l.Error("[ResetTraffic] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) + return nil, errors.Wrapf(err, "find payment error: %v", err.Error()) + } + var feeAmount int64 + // Calculate the handling fee + if amount > 0 { + feeAmount = calculateFee(amount, payment) + } + // create order + orderInfo := order.Order{ + Id: 0, + ParentId: userSubscribe.OrderId, + UserId: u.Id, + OrderNo: tool.GenerateTradeNo(), + Type: 3, + Price: userSubscribe.Subscribe.Replacement, + Amount: amount + feeAmount, + GiftAmount: deductionAmount, + FeeAmount: feeAmount, + PaymentId: req.Payment, + Method: payment.Platform, + Status: 1, + SubscribeId: userSubscribe.SubscribeId, + SubscribeToken: userSubscribe.Token, + } + // Database transaction + err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error { + // update user deduction && Pre deduction ,Return after canceling the order + if orderInfo.GiftAmount > 0 { + // update user deduction && Pre deduction ,Return after canceling the order + if err := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil { + l.Error("[ResetTraffic] Database update error", logger.Field("error", err.Error()), logger.Field("user", u)) + return err + } + // create deduction record + deductionLog := user.GiftAmountLog{ + UserId: orderInfo.UserId, + OrderNo: orderInfo.OrderNo, + Amount: orderInfo.GiftAmount, + Type: 2, + Balance: u.GiftAmount, + Remark: "ResetTraffic order deduction", + } + if err := db.Model(&user.GiftAmountLog{}).Create(&deductionLog).Error; err != nil { + l.Error("[ResetTraffic] Database insert error", logger.Field("error", err.Error()), logger.Field("deductionLog", deductionLog)) + return err + } + } + // insert order + return db.Model(&order.Order{}).Create(&orderInfo).Error + }) + if err != nil { + l.Error("[ResetTraffic] Database insert error", logger.Field("error", err.Error()), logger.Field("order", orderInfo)) + return nil, errors.Wrapf(err, "insert order error: %v", err.Error()) + } + // Deferred task + payload := queue.DeferCloseOrderPayload{ + OrderNo: orderInfo.OrderNo, + } + val, err := json.Marshal(payload) + if err != nil { + l.Error("[ResetTraffic] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload)) + } + task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3)) + taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute)) + if err != nil { + l.Error("[ResetTraffic] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task)) + } else { + l.Info("[ResetTraffic] Enqueue task success", logger.Field("TaskID", taskInfo.ID)) + } + return &types.ResetTrafficOrderResponse{ + OrderNo: orderInfo.OrderNo, + }, nil +} diff --git a/internal/logic/app/payment/getAvailablePaymentMethodsLogic.go b/internal/logic/app/payment/getAvailablePaymentMethodsLogic.go new file mode 100644 index 0000000..8d8a6d5 --- /dev/null +++ b/internal/logic/app/payment/getAvailablePaymentMethodsLogic.go @@ -0,0 +1,40 @@ +package payment + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetAvailablePaymentMethodsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewGetAvailablePaymentMethodsLogic Get available payment methods +func NewGetAvailablePaymentMethodsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAvailablePaymentMethodsLogic { + return &GetAvailablePaymentMethodsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAvailablePaymentMethodsLogic) GetAvailablePaymentMethods() (resp *types.GetAvailablePaymentMethodsResponse, err error) { + data, err := l.svcCtx.PaymentModel.FindAvailableMethods(l.ctx) + if err != nil { + l.Error("[GetAvailablePaymentMethods] database error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetAvailablePaymentMethods: %v", err.Error()) + } + resp = &types.GetAvailablePaymentMethodsResponse{ + List: make([]types.PaymentMethod, 0), + } + tool.DeepCopy(&resp.List, data) + return +} diff --git a/internal/logic/app/subscribe/queryApplicationConfigLogic.go b/internal/logic/app/subscribe/queryApplicationConfigLogic.go new file mode 100644 index 0000000..4f091f4 --- /dev/null +++ b/internal/logic/app/subscribe/queryApplicationConfigLogic.go @@ -0,0 +1,115 @@ +package subscribe + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/application" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type QueryApplicationConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get application config +func NewQueryApplicationConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryApplicationConfigLogic { + return &QueryApplicationConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryApplicationConfigLogic) QueryApplicationConfig() (resp *types.ApplicationResponse, err error) { + resp = &types.ApplicationResponse{} + var applications []*application.Application + err = l.svcCtx.ApplicationModel.Transaction(l.ctx, func(tx *gorm.DB) (err error) { + return tx.Model(applications).Preload("ApplicationVersions").Find(&applications).Error + }) + if err != nil { + l.Errorw("[QueryApplicationConfig] get application error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get application error: %v", err.Error()) + } + + if len(applications) == 0 { + return resp, nil + } + + for _, app := range applications { + applicationResponse := types.ApplicationResponseInfo{ + Id: app.Id, + Name: app.Name, + Icon: app.Icon, + Description: app.Description, + SubscribeType: app.SubscribeType, + } + applicationVersions := app.ApplicationVersions + if len(applicationVersions) != 0 { + for _, applicationVersion := range applicationVersions { + /*if !applicationVersion.IsDefault { + continue + }*/ + switch applicationVersion.Platform { + case "ios": + applicationResponse.Platform.IOS = append(applicationResponse.Platform.IOS, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + case "macos": + applicationResponse.Platform.MacOS = append(applicationResponse.Platform.MacOS, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + case "linux": + applicationResponse.Platform.Linux = append(applicationResponse.Platform.Linux, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + case "android": + applicationResponse.Platform.Android = append(applicationResponse.Platform.Android, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + case "windows": + applicationResponse.Platform.Windows = append(applicationResponse.Platform.Windows, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + case "harmony": + applicationResponse.Platform.Harmony = append(applicationResponse.Platform.Harmony, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + } + } + } + resp.Applications = append(resp.Applications, applicationResponse) + } + return +} diff --git a/internal/logic/app/subscribe/querySubscribeGroupListLogic.go b/internal/logic/app/subscribe/querySubscribeGroupListLogic.go new file mode 100644 index 0000000..d0f08ac --- /dev/null +++ b/internal/logic/app/subscribe/querySubscribeGroupListLogic.go @@ -0,0 +1,44 @@ +package subscribe + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type QuerySubscribeGroupListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get subscribe group list +func NewQuerySubscribeGroupListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QuerySubscribeGroupListLogic { + return &QuerySubscribeGroupListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QuerySubscribeGroupListLogic) QuerySubscribeGroupList() (resp *types.QuerySubscribeGroupListResponse, err error) { + var list []*subscribe.Group + var total int64 + err = l.svcCtx.DB.Model(&subscribe.Group{}).Count(&total).Find(&list).Error + if err != nil { + l.Logger.Error("[QuerySubscribeGroupListLogic] get subscribe group list failed: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get subscribe group list failed: %v", err.Error()) + } + groupList := make([]types.SubscribeGroup, 0) + tool.DeepCopy(&groupList, list) + return &types.QuerySubscribeGroupListResponse{ + Total: total, + List: groupList, + }, nil +} diff --git a/internal/logic/app/subscribe/querySubscribeListLogic.go b/internal/logic/app/subscribe/querySubscribeListLogic.go new file mode 100644 index 0000000..22475d0 --- /dev/null +++ b/internal/logic/app/subscribe/querySubscribeListLogic.go @@ -0,0 +1,55 @@ +package subscribe + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type QuerySubscribeListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get subscribe list +func NewQuerySubscribeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QuerySubscribeListLogic { + return &QuerySubscribeListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QuerySubscribeListLogic) QuerySubscribeList() (resp *types.QuerySubscribeListResponse, err error) { + + data, err := l.svcCtx.SubscribeModel.QuerySubscribeList(l.ctx) + if err != nil { + l.Errorw("[QuerySubscribeListLogic] Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QuerySubscribeList error: %v", err.Error()) + } + resp = &types.QuerySubscribeListResponse{ + List: make([]types.Subscribe, 0), + Total: int64(len(data)), + } + for _, v := range data { + var sub types.Subscribe + tool.DeepCopy(&sub, v) + if v.Discount != "" { + if err = json.Unmarshal([]byte(v.Discount), &sub.Discount); err != nil { + l.Errorw("[QuerySubscribeListLogic] json.Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", v.Discount)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "json.Unmarshal error: %v", err.Error()) + } + } else { + sub.Discount = make([]types.SubscribeDiscount, 0) + } + resp.List = append(resp.List, sub) + } + return +} diff --git a/internal/logic/app/subscribe/queryUserAlreadySubscribeLogic.go b/internal/logic/app/subscribe/queryUserAlreadySubscribeLogic.go new file mode 100644 index 0000000..330213d --- /dev/null +++ b/internal/logic/app/subscribe/queryUserAlreadySubscribeLogic.go @@ -0,0 +1,67 @@ +package subscribe + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/order" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type QueryUserAlreadySubscribeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Already subscribed to package +func NewQueryUserAlreadySubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserAlreadySubscribeLogic { + return &QueryUserAlreadySubscribeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryUserAlreadySubscribeLogic) QueryUserAlreadySubscribe() (resp *types.QueryUserSubscribeResp, err error) { + resp = &types.QueryUserSubscribeResp{ + Data: make([]types.UserSubscribeData, 0), + } + userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) + var orderIds []int64 + var subscribes []user.Subscribe + err = l.svcCtx.OrderModel.Transaction(context.Background(), func(tx *gorm.DB) error { + if err := tx.Model(&order.Order{}).Where("user_id = ? AND status in ?", userInfo.Id, []int64{2, 5}).Select("id").Find(&orderIds).Error; err != nil { + return err + } + if len(orderIds) == 0 { + return nil + } + return tx.Model(&user.Subscribe{}).Where("user_id = ? AND order_id in ?", userInfo.Id, orderIds).Order("created_at desc").Find(&subscribes).Error + }) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find order error: %v", err.Error()) + } + if len(subscribes) == 0 { + return + } + + userAlreadySubscribe := make(map[int64]int64) + for _, subscribe := range subscribes { + userAlreadySubscribe[subscribe.SubscribeId] = subscribe.Id + } + + for k, v := range userAlreadySubscribe { + resp.Data = append(resp.Data, types.UserSubscribeData{ + SubscribeId: k, + UserSubscribeId: v, + }) + } + return +} diff --git a/internal/logic/app/subscribe/queryUserAvailableUserSubscribeLogic.go b/internal/logic/app/subscribe/queryUserAvailableUserSubscribeLogic.go new file mode 100644 index 0000000..a41513a --- /dev/null +++ b/internal/logic/app/subscribe/queryUserAvailableUserSubscribeLogic.go @@ -0,0 +1,118 @@ +package subscribe + +import ( + "context" + "encoding/json" + "strconv" + "strings" + "time" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/countryCenter" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type QueryUserAvailableUserSubscribeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Available subscriptions for users +func NewQueryUserAvailableUserSubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserAvailableUserSubscribeLogic { + return &QueryUserAvailableUserSubscribeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryUserAvailableUserSubscribeLogic) QueryUserAvailableUserSubscribe(req *types.AppUserSubscribeRequest) (resp *types.AppUserSubscbribeResponse, err error) { + resp = &types.AppUserSubscbribeResponse{List: make([]types.AppUserSubcbribe, 0)} + userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) + //查询用户订阅 + subscribeDetails, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, userInfo.Id, 1, 2) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get query user subscribe error: %v", err.Error()) + } + + userSubscribeMap := make(map[int64]types.AppUserSubcbribe) + for _, sd := range subscribeDetails { + userSubscribeInfo := types.AppUserSubcbribe{ + Id: sd.Id, + Name: sd.Subscribe.Name, + Traffic: sd.Traffic, + Upload: sd.Upload, + Download: sd.Download, + ExpireTime: sd.ExpireTime.Format(time.DateTime), + StartTime: sd.StartTime.Format(time.DateTime), + DeviceLimit: sd.Subscribe.DeviceLimit, + } + + //不需要查询节点 + if req.ContainsNodes == nil || !*req.ContainsNodes { + resp.List = append(resp.List, userSubscribeInfo) + continue + } + + //拿到所有订阅下的服务组id + var ids []int64 + for _, idStr := range strings.Split(sd.Subscribe.ServerGroup, ",") { + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + continue + } + ids = append(ids, id) + } + //根据服务组id拿到所有节点 + servers, err := l.svcCtx.ServerModel.FindServerListByGroupIds(l.ctx, ids) + if err != nil { + l.Logger.Errorf("FindServerListByGroupIds error: %v", err.Error()) + continue + } + + for _, server := range servers { + latitude, longitude, found := countryCenter.GetCountryCenterByCountryOrCity(server.Country, server.City) + if !found { + latitude = server.Latitude + longitude = server.Longitude + } + userSubscribeInfo.List = append(userSubscribeInfo.List, types.AppUserSubscbribeNode{ + Id: server.Id, + Uuid: sd.UUID, + Traffic: sd.Traffic, + Upload: sd.Upload, + Download: sd.Download, + RelayNode: server.RelayNode, + RelayMode: server.RelayMode, + Longitude: server.Longitude, + Latitude: server.Latitude, + LatitudeCountry: latitude, + LongitudeCountry: longitude, + Tags: strings.Split(server.Tags, ","), + Config: server.Config, + ServerAddr: server.ServerAddr, + Protocol: server.Protocol, + SpeedLimit: server.SpeedLimit, + City: server.City, + Country: server.Country, + Name: server.Name, + }) + } + resp.List = append(resp.List, userSubscribeInfo) + userSubscribeMap[userSubscribeInfo.Id] = userSubscribeInfo + } + + for _, userSubscribeInfo := range userSubscribeMap { + resp.List = append(resp.List, userSubscribeInfo) + } + data, _ := json.Marshal(resp) + l.Logger.Infof("QueryUserAvailableUserSubscribe resp: %s", string(data)) + return resp, nil + +} diff --git a/internal/logic/app/subscribe/resetUserSubscribePeriodLogic.go b/internal/logic/app/subscribe/resetUserSubscribePeriodLogic.go new file mode 100644 index 0000000..1b5e8f5 --- /dev/null +++ b/internal/logic/app/subscribe/resetUserSubscribePeriodLogic.go @@ -0,0 +1,60 @@ +package subscribe + +import ( + "context" + "time" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type ResetUserSubscribePeriodLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewResetUserSubscribePeriodLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetUserSubscribePeriodLogic { + return &ResetUserSubscribePeriodLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ResetUserSubscribePeriodLogic) ResetUserSubscribePeriod(req *types.UserSubscribeResetPeriodRequest) (resp *types.UserSubscribeResetPeriodResponse, err error) { + resp = &types.UserSubscribeResetPeriodResponse{} + userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) + subscribe, err := l.svcCtx.UserModel.FindOneSubscribe(l.ctx, req.UserSubscribeId) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find order error: %v", err.Error()) + } + if userInfo.Id != subscribe.UserId { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeNotAvailable), "user not authorized,subscribe not available") + } + + if time.Now().After(subscribe.ExpireTime) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeExpired), "subscribe expired") + } + + if subscribe.Traffic < 1 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ExistAvailableTraffic), "Unlimited data plan.") + } + + if (subscribe.Download + subscribe.Upload + 10240) < subscribe.Traffic { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ExistAvailableTraffic), "There is still available traffic.") + } + + subscribe.ExpireTime = subscribe.ExpireTime.AddDate(0, -1, 0) + err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, subscribe) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update subscribe error: %v", err.Error()) + } + resp.Status = true + return +} diff --git a/internal/logic/app/user/deleteAccountLogic.go b/internal/logic/app/user/deleteAccountLogic.go new file mode 100644 index 0000000..fdc04e3 --- /dev/null +++ b/internal/logic/app/user/deleteAccountLogic.go @@ -0,0 +1,103 @@ +package user + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/logic/common" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type DeleteAccountLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Delete Account +func NewDeleteAccountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteAccountLogic { + return &DeleteAccountLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteAccountLogic) DeleteAccount(req *types.DeleteAccountRequest) error { + userInfo, exists := l.ctx.Value(constant.CtxKeyUser).(user.User) + if !exists { + return nil + } + + var account string + for _, authMethod := range userInfo.AuthMethods { + if authMethod.AuthType == req.Method { + account = authMethod.AuthIdentifier + break + } + } + if account == "" { + return nil + } + + if req.Method == "email" { + emailConfig := l.svcCtx.Config.Email + + if !emailConfig.Enable { + return errors.Wrapf(xerr.NewErrCode(xerr.EmailNotEnabled), "Email function is not enabled yet") + } + + if emailConfig.EnableVerify { + cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Security, account) + value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() + if err != nil { + l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) + return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + + var payload common.CacheKeyPayload + err = json.Unmarshal([]byte(value), &payload) + if err != nil { + l.Errorw("Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) + return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + + if payload.Code != req.Code { + return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + } + } else { + cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.Security, account) + value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() + if err != nil { + l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) + return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + + if value == "" { + return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + + var payload common.CacheKeyPayload + if err := json.Unmarshal([]byte(value), &payload); err != nil { + l.Errorw("[SendSmsCode]: Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) + } + if payload.Code != req.Code { + return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + } + err := l.svcCtx.UserModel.Delete(l.ctx, userInfo.Id) + if err != nil { + l.Errorw("update user password error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update user password") + } + return nil +} diff --git a/internal/logic/app/user/getuseronlinetimestatisticslogic.go b/internal/logic/app/user/getuseronlinetimestatisticslogic.go new file mode 100644 index 0000000..e875dfd --- /dev/null +++ b/internal/logic/app/user/getuseronlinetimestatisticslogic.go @@ -0,0 +1,115 @@ +package user + +import ( + "context" + "sort" + "time" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type GetUserOnlineTimeStatisticsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get user online time total +func NewGetUserOnlineTimeStatisticsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserOnlineTimeStatisticsLogic { + return &GetUserOnlineTimeStatisticsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetUserOnlineTimeStatisticsLogic) GetUserOnlineTimeStatistics() (resp *types.GetUserOnlineTimeStatisticsResponse, err error) { + u := l.ctx.Value(constant.CtxKeyUser).(*user.User) + //获取历史最长在线时间 + var OnlineSeconds int64 + if err := l.svcCtx.DB.Model(user.DeviceOnlineRecord{}).Where("user_id = ?", u.Id).Select("online_seconds").Order("online_seconds desc").Limit(1).Scan(&OnlineSeconds).Error; err != nil { + l.Logger.Error(err) + } + + //获取历史连续最长在线天数 + var DurationDays int64 + if err := l.svcCtx.DB.Model(user.DeviceOnlineRecord{}).Where("user_id = ?", u.Id).Select("duration_days").Order("duration_days desc").Limit(1).Scan(&DurationDays).Error; err != nil { + l.Logger.Error(err) + } + + //获取近七天在线情况 + var userOnlineRecord []user.DeviceOnlineRecord + if err := l.svcCtx.DB.Model(&userOnlineRecord).Where("user_id = ? and created_at >= ?", u.Id, time.Now().AddDate(0, 0, -7).Format(time.DateTime)).Order("created_at desc").Find(&userOnlineRecord).Error; err != nil { + l.Logger.Error(err) + } + + //获取当前连续在线天数 + var currentContinuousDays int64 + if len(userOnlineRecord) > 0 { + currentContinuousDays = userOnlineRecord[0].DurationDays + } else { + currentContinuousDays = 1 + } + + var dates []string + for i := 0; i < 7; i++ { + date := time.Now().AddDate(0, 0, -i).Format(time.DateOnly) + dates = append(dates, date) + } + + onlineDays := make(map[string]types.WeeklyStat) + for _, record := range userOnlineRecord { + //获取近七天在线情况 + onlineTime := record.OnlineTime.Format(time.DateOnly) + if weeklyStat, ok := onlineDays[onlineTime]; ok { + weeklyStat.Hours += float64(record.OnlineSeconds) + onlineDays[onlineTime] = weeklyStat + } else { + onlineDays[onlineTime] = types.WeeklyStat{ + Hours: float64(record.OnlineSeconds), + //根据日期获取周几 + DayName: record.OnlineTime.Weekday().String(), + } + } + } + + //补全不存在的日期 + for _, date := range dates { + if _, ok := onlineDays[date]; !ok { + onlineTime, _ := time.Parse(time.DateOnly, date) + onlineDays[date] = types.WeeklyStat{ + DayName: onlineTime.Weekday().String(), + } + } + } + + var keys []string + for key := range onlineDays { + keys = append(keys, key) + } + + //排序 + sort.Strings(keys) + + var weeklyStats []types.WeeklyStat + for index, key := range keys { + weeklyStat := onlineDays[key] + weeklyStat.Day = index + 1 + weeklyStat.Hours = weeklyStat.Hours / float64(3600) + weeklyStats = append(weeklyStats, weeklyStat) + } + + resp = &types.GetUserOnlineTimeStatisticsResponse{ + WeeklyStats: weeklyStats, + ConnectionRecords: types.ConnectionRecords{ + CurrentContinuousDays: currentContinuousDays, + HistoryContinuousDays: DurationDays, + LongestSingleConnection: OnlineSeconds / 60, + }, + } + return +} diff --git a/internal/logic/app/user/getusersubscribetrafficlogslogic.go b/internal/logic/app/user/getusersubscribetrafficlogslogic.go new file mode 100644 index 0000000..4c8a196 --- /dev/null +++ b/internal/logic/app/user/getusersubscribetrafficlogslogic.go @@ -0,0 +1,85 @@ +package user + +import ( + "context" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/perfect-panel/ppanel-server/internal/model/traffic" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "gorm.io/gorm" +) + +type GetUserSubscribeTrafficLogsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get user subcribe traffic logs +func NewGetUserSubscribeTrafficLogsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserSubscribeTrafficLogsLogic { + return &GetUserSubscribeTrafficLogsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetUserSubscribeTrafficLogsLogic) GetUserSubscribeTrafficLogs(req *types.GetUserSubscribeTrafficLogsRequest) (resp *types.GetUserSubscribeTrafficLogsResponse, err error) { + resp = &types.GetUserSubscribeTrafficLogsResponse{} + u := l.ctx.Value(constant.CtxKeyUser).(*user.User) + var traffics []traffic.TrafficLog + err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + return db.Model(traffic.TrafficLog{}).Where("user_id = ? and `timestamp` >= ? and `timestamp` < ?", u.Id, time.UnixMilli(req.StartTime), time.UnixMilli(req.EndTime)).Find(&traffics).Error + }) + + if err != nil { + l.Errorw("get user subscribe traffic logs failed", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error()) + } + + //合并多条记录为以天为单位 + trafficMap := make(map[string]*traffic.TrafficLog) + for _, traf := range traffics { + key := traf.Timestamp.Format(time.DateOnly) + existTraf := trafficMap[key] + if existTraf == nil { + trafficMap[key] = &traf + } else { + existTraf.Upload = existTraf.Download + traf.Upload + existTraf.Download = existTraf.Download + traf.Download + trafficMap[key] = existTraf + } + } + + startTime := time.UnixMilli(req.StartTime) + EndTime := time.UnixMilli(req.EndTime) + res := make(map[string]traffic.TrafficLog) + + // 循环遍历每一天 + for current := startTime; !current.After(EndTime); current = current.AddDate(0, 0, 1) { + dateStr := current.Format(time.DateOnly) // 格式化为日期字符串 + if trafficMap[dateStr] == nil { + res[dateStr] = traffic.TrafficLog{ + Timestamp: current, + } + } else { + res[dateStr] = *trafficMap[dateStr] + } + resp.List = append(resp.List, types.TrafficLog{ + Id: res[dateStr].Id, + ServerId: res[dateStr].ServerId, + Upload: res[dateStr].Upload, + Download: res[dateStr].Download, + Timestamp: res[dateStr].Timestamp.UnixMilli(), + }) + } + + return +} diff --git a/internal/logic/app/user/queryUserAffiliateListLogic.go b/internal/logic/app/user/queryUserAffiliateListLogic.go new file mode 100644 index 0000000..4ef7a41 --- /dev/null +++ b/internal/logic/app/user/queryUserAffiliateListLogic.go @@ -0,0 +1,62 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type QueryUserAffiliateListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Query User Affiliate List +func NewQueryUserAffiliateListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserAffiliateListLogic { + return &QueryUserAffiliateListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryUserAffiliateListLogic) QueryUserAffiliateList(req *types.QueryUserAffiliateListRequest) (resp *types.QueryUserAffiliateListResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + var data []*user.User + var total int64 + err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + return db.Model(&user.User{}).Order("id desc").Where("referer_id = ?", u.Id).Count(&total).Limit(req.Size).Offset((req.Page - 1) * req.Size).Find(&data).Error + }) + if err != nil { + l.Errorw("Query User Affiliate List failed: %v", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Affiliate List failed: %v", err.Error()) + } + + list := make([]types.UserAffiliate, 0) + for _, item := range data { + list = append(list, types.UserAffiliate{ + //Email: tool.MaskEmail(item.Email), + Avatar: item.Avatar, + RegisteredAt: item.CreatedAt.UnixMilli(), + Enable: *item.Enable, + }) + } + return &types.QueryUserAffiliateListResponse{ + Total: total, + List: list, + }, nil +} diff --git a/internal/logic/app/user/queryUserInfoLogic.go b/internal/logic/app/user/queryUserInfoLogic.go new file mode 100644 index 0000000..76fea78 --- /dev/null +++ b/internal/logic/app/user/queryUserInfoLogic.go @@ -0,0 +1,63 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type QueryUserInfoLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// query user info +func NewQueryUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserInfoLogic { + return &QueryUserInfoLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryUserInfoLogic) QueryUserInfo() (resp *types.UserInfoResponse, err error) { + u := l.ctx.Value(constant.CtxKeyUser).(*user.User) + var devices []types.UserDevice + if len(u.UserDevices) != 0 { + for _, device := range u.UserDevices { + devices = append(devices, types.UserDevice{ + Id: device.Id, + Identifier: device.Identifier, + Online: device.Online, + }) + } + } + var authMeths []types.UserAuthMethod + authMethods, err := l.svcCtx.UserModel.FindUserAuthMethods(l.ctx, u.Id) + if err == nil && len(authMeths) != 0 { + for _, as := range authMethods { + authMeths = append(authMeths, types.UserAuthMethod{ + AuthType: as.AuthType, + AuthIdentifier: as.AuthIdentifier, + }) + } + } + + resp = &types.UserInfoResponse{ + Id: u.Id, + Balance: u.Balance, + Avatar: u.Avatar, + ReferCode: u.ReferCode, + RefererId: u.RefererId, + Devices: devices, + AuthMethods: authMeths, + } + return +} diff --git a/internal/logic/app/user/queryuseraffiliatelogic.go b/internal/logic/app/user/queryuseraffiliatelogic.go new file mode 100644 index 0000000..839a81d --- /dev/null +++ b/internal/logic/app/user/queryuseraffiliatelogic.go @@ -0,0 +1,60 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type QueryUserAffiliateLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Query User Affiliate Count +func NewQueryUserAffiliateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserAffiliateLogic { + return &QueryUserAffiliateLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryUserAffiliateLogic) QueryUserAffiliate() (resp *types.QueryUserAffiliateCountResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + var sum int64 + var total int64 + err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + return db.Model(&user.User{}).Where("referer_id = ?", u.Id).Count(&total).Find(&user.User{}).Error + }) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Affiliate failed: %v", err) + } + err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + return db.Model(&user.CommissionLog{}). + Where("user_id = ?", u.Id). + Select("COALESCE(SUM(amount), 0)"). + Scan(&sum).Error + }) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Affiliate failed: %v", err) + } + + return &types.QueryUserAffiliateCountResponse{ + Registers: total, + TotalCommission: sum, + }, nil +} diff --git a/internal/logic/app/user/updatePasswordLogic.go b/internal/logic/app/user/updatePasswordLogic.go new file mode 100644 index 0000000..073600b --- /dev/null +++ b/internal/logic/app/user/updatePasswordLogic.go @@ -0,0 +1,46 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdatePasswordLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Update Password +func NewUpdatePasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdatePasswordLogic { + return &UpdatePasswordLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdatePasswordLogic) UpdatePassword(req *types.UpdatePasswordRequeset) error { + userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) + + // Verify password + if !tool.VerifyPassWord(req.Password, userInfo.Password) { + return errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password") + } + userInfo.Password = tool.EncodePassWord(req.NewPassword) + err := l.svcCtx.UserModel.Update(l.ctx, userInfo) + if err != nil { + l.Errorw("update user password error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update user password") + } + return err +} diff --git a/internal/logic/app/ws/appWsLogic.go b/internal/logic/app/ws/appWsLogic.go new file mode 100644 index 0000000..8f474da --- /dev/null +++ b/internal/logic/app/ws/appWsLogic.go @@ -0,0 +1,81 @@ +package ws + +import ( + "context" + "net/http" + "strconv" + "time" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type AppWsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// App heartbeat +func NewAppWsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AppWsLogic { + return &AppWsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AppWsLogic) AppWs(w http.ResponseWriter, r *http.Request, userid, identifier string) error { + //获取设备号 + if identifier == "" { + return xerr.NewErrCode(xerr.DeviceNotExist) + } + //获取用户id + userID, err := strconv.ParseInt(userid, 10, 64) + if err != nil { + return xerr.NewErrCode(xerr.UseridNotMatch) + } + + ////获取session + value := l.ctx.Value(constant.CtxKeySessionID) + if value == nil { + return xerr.NewErrCode(xerr.ErrorTokenInvalid) + } + session := value.(string) + + //获取用户 + userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) + + if userID != userInfo.Id { + return xerr.NewErrCode(xerr.UseridNotMatch) + } + + _, err = l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, identifier) + if err != nil { + return xerr.NewErrCode(xerr.DeviceNotExist) + } + + //if device.UserId != userInfo.Id { + // return xerr.NewErrCode(xerr.DeviceNotExist) + //} + + //默认在线设备1 + maxDevice := 3 + subscribe, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, userInfo.Id, 1, 2) + if err == nil { + for _, sub := range subscribe { + if time.Now().Before(sub.ExpireTime) { + deviceLimit := int(sub.Subscribe.DeviceLimit) + if deviceLimit > maxDevice { + maxDevice = deviceLimit + } + } + } + } + l.svcCtx.DeviceManager.AddDevice(w, r, session, userID, identifier, maxDevice) + return nil +} diff --git a/internal/logic/auth/checkUserLogic.go b/internal/logic/auth/checkUserLogic.go new file mode 100644 index 0000000..c1503b8 --- /dev/null +++ b/internal/logic/auth/checkUserLogic.go @@ -0,0 +1,37 @@ +package auth + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type CheckUserLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewCheckUserLogic Check user is exist +func NewCheckUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CheckUserLogic { + return &CheckUserLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CheckUserLogic) CheckUser(req *types.CheckUserRequest) (resp *types.CheckUserResponse, err error) { + authMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "email", req.Email) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user by email error: %v", err.Error()) + } + return &types.CheckUserResponse{ + Exist: authMethod.UserId != 0, + }, nil +} diff --git a/internal/logic/auth/checkUserTelephoneLogic.go b/internal/logic/auth/checkUserTelephoneLogic.go new file mode 100644 index 0000000..9c42fc6 --- /dev/null +++ b/internal/logic/auth/checkUserTelephoneLogic.go @@ -0,0 +1,44 @@ +package auth + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/phone" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type CheckUserTelephoneLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Check user telephone is exist +func NewCheckUserTelephoneLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CheckUserTelephoneLogic { + return &CheckUserTelephoneLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CheckUserTelephoneLogic) CheckUserTelephone(req *types.TelephoneCheckUserRequest) (resp *types.TelephoneCheckUserResponse, err error) { + phoneNumber, err := phone.FormatToE164(req.TelephoneAreaCode, req.Telephone) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number") + } + authMethods, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "mobile", phoneNumber) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user by email error: %v", err.Error()) + } + + return &types.TelephoneCheckUserResponse{ + Exist: authMethods.UserId != 0, + }, nil +} diff --git a/internal/logic/auth/oauth/appleLoginCallbackLogic.go b/internal/logic/auth/oauth/appleLoginCallbackLogic.go new file mode 100644 index 0000000..38c825f --- /dev/null +++ b/internal/logic/auth/oauth/appleLoginCallbackLogic.go @@ -0,0 +1,40 @@ +package oauth + +import ( + "context" + "fmt" + "net/http" + "net/url" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type AppleLoginCallbackLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Apple Login Callback +func NewAppleLoginCallbackLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AppleLoginCallbackLogic { + return &AppleLoginCallbackLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AppleLoginCallbackLogic) AppleLoginCallback(req *types.AppleLoginCallbackRequest, r *http.Request, w http.ResponseWriter) error { + // validate the state code + result, err := l.svcCtx.Redis.Get(l.ctx, fmt.Sprintf("apple:%s", req.State)).Result() + if err != nil { + l.Errorw("get apple state code from redis failed", logger.Field("error", err.Error()), logger.Field("code", req.State)) + http.Redirect(w, r, l.svcCtx.Config.Site.Host, http.StatusTemporaryRedirect) + return nil + } + http.Redirect(w, r, fmt.Sprintf("%s?method=apple&code=%s&state=%s", result, req.Code, req.State), http.StatusFound) + l.Infow("redirect to apple login page", logger.Field("url", fmt.Sprintf("%s?method=apple&code=%s&state=%s", result, url.QueryEscape(req.Code), req.State))) + return nil +} diff --git a/internal/logic/auth/oauth/oAuthLoginGetTokenLogic.go b/internal/logic/auth/oauth/oAuthLoginGetTokenLogic.go new file mode 100644 index 0000000..4fd9833 --- /dev/null +++ b/internal/logic/auth/oauth/oAuthLoginGetTokenLogic.go @@ -0,0 +1,344 @@ +package oauth + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/model/auth" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/jwt" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/oauth/apple" + "github.com/perfect-panel/ppanel-server/pkg/oauth/google" + "github.com/perfect-panel/ppanel-server/pkg/oauth/telegram" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/uuidx" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type googleRequest struct { + Code string `json:"code"` + State string `json:"state"` +} +type OAuthLoginGetTokenLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewOAuthLoginGetTokenLogic OAuth login get token +func NewOAuthLoginGetTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *OAuthLoginGetTokenLogic { + return &OAuthLoginGetTokenLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *OAuthLoginGetTokenLogic) OAuthLoginGetToken(req *types.OAuthLoginGetTokenRequest, ip, userAgent string) (resp *types.LoginResponse, err error) { + loginStatus := false + var userInfo *user.User + // Record login status + defer func(svcCtx *svc.ServiceContext) { + if userInfo != nil && userInfo.Id != 0 { + if err := svcCtx.UserModel.InsertLoginLog(l.ctx, &user.LoginLog{ + UserId: userInfo.Id, + LoginIP: ip, + UserAgent: userAgent, + Success: &loginStatus, + }); err != nil { + l.Errorw("error insert login log: %v", logger.Field("error", err.Error())) + } + } + }(l.svcCtx) + switch req.Method { + case "google": + userInfo, err = l.google(req) + case "apple": + userInfo, err = l.apple(req) + case "telegram": + userInfo, err = l.telegram(req) + default: + l.Errorw("oauth login method not support: %v", logger.Field("method", req.Method)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "oauth login method not support: %v", req.Method) + } + if err != nil { + return nil, err + } + // Generate session id + sessionId := uuidx.NewUUID().String() + // Generate token + token, err := jwt.NewJwtToken( + l.svcCtx.Config.JwtAuth.AccessSecret, + time.Now().Unix(), + l.svcCtx.Config.JwtAuth.AccessExpire, + jwt.WithOption("UserId", userInfo.Id), + jwt.WithOption("SessionId", sessionId), + ) + if err != nil { + l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) + } + + sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) + if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) + } + loginStatus = true + return &types.LoginResponse{ + Token: token, + }, nil +} + +func (l *OAuthLoginGetTokenLogic) google(req *types.OAuthLoginGetTokenRequest) (*user.User, error) { + var request googleRequest + err := tool.CloneMapToStruct(req.Callback.(map[string]interface{}), &request) + if err != nil { + l.Errorw("error CloneMapToStruct: %v", logger.Field("error", err.Error())) + return nil, err + } + // validate the state code + redirect, err := l.svcCtx.Redis.Get(l.ctx, fmt.Sprintf("google:%s", request.State)).Result() + if err != nil { + l.Errorw("error get google state code: %v", logger.Field("error", err.Error())) + return nil, err + } + // get google config + authMethod, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, "google") + if err != nil { + l.Errorw("error find google auth method: %v", logger.Field("error", err.Error())) + return nil, err + } + var cfg auth.GoogleAuthConfig + err = cfg.Unmarshal(authMethod.Config) + if err != nil { + l.Errorw("error unmarshal google config: %v", logger.Field("config", authMethod.Config), logger.Field("error", err.Error())) + return nil, err + } + client := google.New(&google.Config{ + ClientID: cfg.ClientId, + ClientSecret: cfg.ClientSecret, + RedirectURL: redirect, + }) + token, err := client.Exchange(l.ctx, request.Code) + if err != nil { + l.Errorw("error exchange google token: %v", logger.Field("error", err.Error())) + return nil, err + } + googleUserInfo, err := client.GetUserInfo(token.AccessToken) + if err != nil { + l.Errorw("error get google user info: %v", logger.Field("error", err.Error())) + return nil, err + } + // query user info + userAuthMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "google", googleUserInfo.OpenID) + if err != nil { + if errors.As(err, &gorm.ErrRecordNotFound) { + return l.register(googleUserInfo.Email, googleUserInfo.Picture, "google", googleUserInfo.OpenID) + } + return nil, err + } + return l.svcCtx.UserModel.FindOne(l.ctx, userAuthMethod.UserId) +} + +func (l *OAuthLoginGetTokenLogic) apple(req *types.OAuthLoginGetTokenRequest) (*user.User, error) { + // validate the state code + _, err := l.svcCtx.Redis.Get(l.ctx, fmt.Sprintf("apple:%s", req.Callback.(map[string]interface{})["state"])).Result() + if err != nil { + l.Errorw("[AppleLoginCallback] Get State code error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "get apple state code failed: %v", err.Error()) + } + appleAuth, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, "apple") + if err != nil { + l.Errorw("[AppleLoginCallback] FindOneByMethod error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find apple auth method failed: %v", err.Error()) + } + var appleCfg auth.AppleAuthConfig + err = appleCfg.Unmarshal(appleAuth.Config) + if err != nil { + l.Errorw("[AppleLoginCallback] Unmarshal error", logger.Field("error", err.Error()), logger.Field("config", appleAuth.Config)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "unmarshal apple config failed: %v", err.Error()) + } + + client, err := apple.New(apple.Config{ + ClientID: appleCfg.ClientId, + TeamID: appleCfg.TeamID, + KeyID: appleCfg.KeyID, + ClientSecret: appleCfg.ClientSecret, + RedirectURI: appleCfg.RedirectURL, + }) + if err != nil { + l.Errorw("[AppleLoginCallback] New apple client error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "new apple client failed: %v", err.Error()) + } + // verify web token + resp, err := client.VerifyWebToken(l.ctx, req.Callback.(map[string]interface{})["code"].(string)) + if err != nil { + l.Errorw("[AppleLoginCallback] VerifyWebToken error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "verify web token failed: %v", err.Error()) + } + if resp.Error != "" { + l.Errorw("[AppleLoginCallback] VerifyWebToken error", logger.Field("error", resp.Error)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "verify web token failed: %v", resp.Error) + } + // query apple user unique id + appleUnique, err := apple.GetUniqueID(resp.IDToken) + if err != nil { + l.Errorw("[AppleLoginCallback] GetUniqueID error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "get apple unique id failed: %v", err.Error()) + } + // get apple user info + appleUserInfo, err := apple.GetClaims(resp.AccessToken) + if err != nil { + l.Errorw("[AppleLoginCallback] GetClaims error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "get apple user info failed: %v", err.Error()) + } + // query user by apple unique id + userAuthMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "apple", appleUnique) + if err != nil { + // if user not exist, handle register + if errors.Is(err, gorm.ErrRecordNotFound) { + return l.register((*appleUserInfo)["email"].(string), "", "apple", appleUnique) + } + l.Errorw("[AppleLoginCallback] FindUserAuthMethodByOpenID error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user auth method by openid failed: %v", err.Error()) + } + // query user info + userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, userAuthMethod.UserId) + + if err != nil { + l.Errorw( + "[AppleLoginCallback] FindOne error", + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user info failed: %v", err.Error()) + } + + return userInfo, nil +} + +func (l *OAuthLoginGetTokenLogic) telegram(req *types.OAuthLoginGetTokenRequest) (*user.User, error) { + appleAuth, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, "telegram") + if err != nil { + l.Errorw("[OAuthLoginGetToken] FindOneByMethod error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find telegram auth method failed: %v", err.Error()) + } + var telegramCfg auth.TelegramAuthConfig + err = json.Unmarshal([]byte(appleAuth.Config), &telegramCfg) + if err != nil { + l.Errorw("[OAuthLoginGetToken] Unmarshal error", logger.Field("error", err.Error()), logger.Field("config", appleAuth.Config)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "unmarshal telegram config failed: %v", err.Error()) + } + encodeText := req.Callback.(map[string]interface{})["tgAuthResult"].(string) + // base64 decode + callbackData, err := telegram.ParseAndValidateBase64([]byte(encodeText), telegramCfg.BotToken) + if err != nil { + l.Errorw("[TelegramLoginCallback] ParseAndValidateBase64 error", logger.Field("error", err.Error())) + return nil, err + } + // 验证数据有效期 + if time.Now().Unix()-*callbackData.AuthDate > 86400 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "auth date expired") + } + // query user auth info + userAuthMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "telegram", fmt.Sprintf("%v", *callbackData.Id)) + if err != nil { + if errors.As(err, &gorm.ErrRecordNotFound) { + return l.register(fmt.Sprintf("%v@%s", *callbackData.Id, "qq.com"), *callbackData.PhotoUrl, "telegram", fmt.Sprintf("%v", callbackData.Id)) + } + } + // query user info + userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, userAuthMethod.UserId) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user info failed: %v", err.Error()) + } + return userInfo, nil +} + +func (l *OAuthLoginGetTokenLogic) register(email, avatar, method, openid string) (*user.User, error) { + if l.svcCtx.Config.Invite.ForcedInvite { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InviteCodeError), "invite code is required") + } + var userInfo *user.User + err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + err := db.Model(&user.User{}).Where("email = ?", email).First(&userInfo).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + if userInfo.Id != 0 { + return errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "user email exist: %v", email) + } + userInfo = &user.User{ + Avatar: avatar, + } + if err := db.Create(userInfo).Error; err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create user info failed: %v", err.Error()) + } + // Generate ReferCode + userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id) + // Update ReferCode + err = db.Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update refer code failed: %v", err.Error()) + } + authMethod := &user.AuthMethods{ + UserId: userInfo.Id, + AuthType: method, + AuthIdentifier: openid, + Verified: true, + } + if err = db.Create(authMethod).Error; err != nil { + l.Errorw("error create auth method: %v", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create auth method failed: %v", err.Error()) + } + if email != "" { + authMethod = &user.AuthMethods{ + UserId: userInfo.Id, + AuthType: "email", + AuthIdentifier: email, + Verified: true, + } + if err := db.Create(authMethod).Error; err != nil { + l.Errorw("error create auth method: %v", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create auth method failed: %v", err.Error()) + } + } + if l.svcCtx.Config.Register.EnableTrial { + // Active trial + if err = l.activeTrial(userInfo.Id); err != nil { + return err + } + } + return nil + }) + return userInfo, err +} + +func (l *OAuthLoginGetTokenLogic) activeTrial(uid int64) error { + sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe) + if err != nil { + return err + } + userSub := &user.Subscribe{ + Id: 0, + UserId: uid, + OrderId: 0, + SubscribeId: sub.Id, + StartTime: time.Now(), + ExpireTime: tool.AddTime(l.svcCtx.Config.Register.TrialTimeUnit, l.svcCtx.Config.Register.TrialTime, time.Now()), + Traffic: sub.Traffic, + Download: 0, + Upload: 0, + Token: uuidx.SubscribeToken(fmt.Sprintf("Trial-%v", uid)), + UUID: uuidx.NewUUID().String(), + Status: 1, + } + return l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub) +} diff --git a/internal/logic/auth/oauth/oAuthLoginLogic.go b/internal/logic/auth/oauth/oAuthLoginLogic.go new file mode 100644 index 0000000..5d2e6a4 --- /dev/null +++ b/internal/logic/auth/oauth/oAuthLoginLogic.go @@ -0,0 +1,134 @@ +package oauth + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/perfect-panel/ppanel-server/internal/model/auth" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/oauth/google" + "github.com/perfect-panel/ppanel-server/pkg/oauth/telegram" + "github.com/perfect-panel/ppanel-server/pkg/random" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "golang.org/x/oauth2" +) + +type OAuthLoginLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// OAuth login +func NewOAuthLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *OAuthLoginLogic { + return &OAuthLoginLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *OAuthLoginLogic) OAuthLogin(req *types.OAthLoginRequest) (resp *types.OAuthLoginResponse, err error) { + var uri string + switch req.Method { + case "google": + uri, err = l.google(req) + case "apple": + uri, err = l.apple(req) + case "telegram": + uri, err = l.telegram(req) + case "github": + uri, err = l.github() + case "facebook": + uri, err = l.facebook() + + } + if err != nil { + l.Errorw("OAuthLogin ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "OAuthLogin: %v", err.Error()) + } + return &types.OAuthLoginResponse{ + Redirect: uri, + }, nil +} + +func (l *OAuthLoginLogic) google(req *types.OAthLoginRequest) (string, error) { + authMethod, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, "google") + if err != nil { + return "", err + } + var cfg auth.GoogleAuthConfig + err = json.Unmarshal([]byte(authMethod.Config), &cfg) + if err != nil { + l.Errorw("error unmarshal google config: %v", logger.Field("config", authMethod.Config), logger.Field("error", err.Error())) + return "", err + } + client := google.New(&google.Config{ + ClientID: cfg.ClientId, + ClientSecret: cfg.ClientSecret, + RedirectURL: req.Redirect, + }) + // generate the state code + code := random.KeyNew(8, 1) + // save the state code + err = l.svcCtx.Redis.Set(l.ctx, fmt.Sprintf("google:%s", code), req.Redirect, 5*60*time.Second).Err() + if err != nil { + return "", err + } + uri := client.AuthCodeURL(code, oauth2.AccessTypeOffline) + return uri, nil +} + +func (l *OAuthLoginLogic) facebook() (string, error) { + return "", nil +} +func (l *OAuthLoginLogic) apple(req *types.OAthLoginRequest) (string, error) { + authMethod, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, "apple") + if err != nil { + return "", err + } + var cfg auth.AppleAuthConfig + err = json.Unmarshal([]byte(authMethod.Config), &cfg) + if err != nil { + l.Errorw("error unmarshal apple config: %v", logger.Field("config", authMethod.Config), logger.Field("error", err.Error())) + return "", err + } + uri := "https://appleid.apple.com/auth/authorize?client_id=%s&redirect_uri=%s&response_type=code&state=%s&scope=name email&response_mode=form_post" + // generate the state code + code := random.KeyNew(8, 1) + // save the state code + err = l.svcCtx.Redis.Set(l.ctx, fmt.Sprintf("apple:%s", code), req.Redirect, 5*60*time.Second).Err() + if err != nil { + l.Errorw("error save state code to redis: %v", logger.Field("code", code), logger.Field("error", err.Error())) + } + return fmt.Sprintf(uri, cfg.ClientId, fmt.Sprintf("%s/v1/auth/oauth/callback/apple", cfg.RedirectURL), code), nil +} +func (l *OAuthLoginLogic) github() (string, error) { + return "", nil +} +func (l *OAuthLoginLogic) telegram(req *types.OAthLoginRequest) (string, error) { + authMethod, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, "telegram") + if err != nil { + return "", err + } + var cfg auth.TelegramAuthConfig + err = json.Unmarshal([]byte(authMethod.Config), &cfg) + if err != nil { + l.Errorw("error unmarshal apple config: %v", logger.Field("config", authMethod.Config), logger.Field("error", err.Error())) + return "", err + } + // generate the state code + code := random.KeyNew(8, 1) + // save the state code + err = l.svcCtx.Redis.Set(l.ctx, fmt.Sprintf("apple:%s", code), req.Redirect, 5*60*time.Second).Err() + if err != nil { + l.Errorw("error save state code to redis", logger.Field("code", code), logger.Field("error", err.Error())) + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "error save state code to redis") + } + return telegram.GenerateTelegramOAuthURL(cfg.BotToken, code, req.Redirect), nil +} diff --git a/internal/logic/auth/resetPasswordLogic.go b/internal/logic/auth/resetPasswordLogic.go new file mode 100644 index 0000000..2ea5bc7 --- /dev/null +++ b/internal/logic/auth/resetPasswordLogic.go @@ -0,0 +1,118 @@ +package auth + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/jwt" + "github.com/perfect-panel/ppanel-server/pkg/uuidx" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" +) + +type ResetPasswordLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewResetPasswordLogic Reset password +func NewResetPasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetPasswordLogic { + return &ResetPasswordLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (resp *types.LoginResponse, err error) { + var userInfo *user.User + loginStatus := false + + defer func() { + if userInfo.Id != 0 && loginStatus { + if err := l.svcCtx.UserModel.InsertLoginLog(l.ctx, &user.LoginLog{ + UserId: userInfo.Id, + LoginIP: req.IP, + UserAgent: req.UserAgent, + Success: &loginStatus, + }); err != nil { + l.Logger.Error("[ResetPassword] insert login log error", logger.Field("error", err.Error())) + } + } + }() + + cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Security, req.Email) + // Check the verification code + if value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result(); err != nil { + l.Errorw("Verification code error", logger.Field("cacheKey", cacheKey), logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "Verification code error") + } else { + var payload CacheKeyPayload + if err := json.Unmarshal([]byte(value), &payload); err != nil { + l.Errorw("Unmarshal errors", logger.Field("cacheKey", cacheKey), logger.Field("error", err.Error()), logger.Field("value", value)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "Verification code error") + } + if payload.Code != req.Code { + l.Errorw("Verification code error", logger.Field("cacheKey", cacheKey), logger.Field("error", "Verification code error"), logger.Field("reqCode", req.Code), logger.Field("payloadCode", payload.Code)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "Verification code error") + } + } + + // Check user + authMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "email", req.Email) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user email not exist: %v", req.Email) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user by email error: %v", err.Error()) + } + + userInfo, err = l.svcCtx.UserModel.FindOne(l.ctx, authMethod.UserId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user email not exist: %v", req.Email) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error()) + } + + // Update password + userInfo.Password = tool.EncodePassWord(req.Password) + if err := l.svcCtx.UserModel.Update(l.ctx, userInfo); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update user info failed: %v", err.Error()) + } + // Generate session id + sessionId := uuidx.NewUUID().String() + // Generate token + token, err := jwt.NewJwtToken( + l.svcCtx.Config.JwtAuth.AccessSecret, + time.Now().Unix(), + l.svcCtx.Config.JwtAuth.AccessExpire, + jwt.WithOption("UserId", userInfo.Id), + jwt.WithOption("SessionId", sessionId), + ) + if err != nil { + l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) + } + sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) + if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) + } + loginStatus = true + return &types.LoginResponse{ + Token: token, + }, nil +} diff --git a/internal/logic/auth/telephoneLoginLogic.go b/internal/logic/auth/telephoneLoginLogic.go new file mode 100644 index 0000000..4f9dfc3 --- /dev/null +++ b/internal/logic/auth/telephoneLoginLogic.go @@ -0,0 +1,135 @@ +package auth + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/logic/common" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/jwt" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/phone" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/uuidx" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type TelephoneLoginLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// User Telephone login +func NewTelephoneLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *TelephoneLoginLogic { + return &TelephoneLoginLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r *http.Request, ip string) (resp *types.LoginResponse, err error) { + phoneNumber, err := phone.FormatToE164(req.TelephoneAreaCode, req.Telephone) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number") + } + if !l.svcCtx.Config.Mobile.Enable { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SmsNotEnabled), "sms login is not enabled") + } + loginStatus := false + var userInfo *user.User + // Record login status + defer func(svcCtx *svc.ServiceContext) { + if userInfo.Id != 0 { + if err := svcCtx.UserModel.InsertLoginLog(l.ctx, &user.LoginLog{ + UserId: userInfo.Id, + LoginIP: ip, + UserAgent: r.UserAgent(), + Success: &loginStatus, + }); err != nil { + l.Logger.Error("[UserLogin] insert login log error", logger.Field("error", err.Error())) + } + } + }(l.svcCtx) + + authMethodInfo, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "mobile", phoneNumber) + if err != nil { + if errors.As(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user telephone not exist: %v", req.Telephone) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error()) + } + + userInfo, err = l.svcCtx.UserModel.FindOne(l.ctx, authMethodInfo.UserId) + if err != nil { + if errors.As(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user telephone not exist: %v", req.Telephone) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error()) + } + + if req.Password == "" && req.TelephoneCode == "" { + return nil, xerr.NewErrCodeMsg(xerr.InvalidParams, "password and telephone code is empty") + } + + if req.TelephoneCode == "" { + // Verify password + if !tool.VerifyPassWord(req.Password, userInfo.Password) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password") + } + } else { + cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.ParseVerifyType(uint8(constant.Security)), phoneNumber) + value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() + if err != nil { + l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + + if value == "" { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + + var payload common.CacheKeyPayload + if err := json.Unmarshal([]byte(value), &payload); err != nil { + l.Errorw("[SendSmsCode]: Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) + } + + if payload.Code != req.TelephoneCode { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + l.svcCtx.Redis.Del(l.ctx, cacheKey) + } + + // Generate session id + sessionId := uuidx.NewUUID().String() + // Generate token + token, err := jwt.NewJwtToken( + l.svcCtx.Config.JwtAuth.AccessSecret, + time.Now().Unix(), + l.svcCtx.Config.JwtAuth.AccessExpire, + jwt.WithOption("UserId", userInfo.Id), + jwt.WithOption("SessionId", sessionId), + ) + if err != nil { + l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) + } + sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) + if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) + } + loginStatus = true + return &types.LoginResponse{ + Token: token, + }, nil +} diff --git a/internal/logic/auth/telephoneResetPasswordLogic.go b/internal/logic/auth/telephoneResetPasswordLogic.go new file mode 100644 index 0000000..51ba9d7 --- /dev/null +++ b/internal/logic/auth/telephoneResetPasswordLogic.go @@ -0,0 +1,106 @@ +package auth + +import ( + "context" + "fmt" + "time" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/jwt" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/phone" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/uuidx" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type TelephoneResetPasswordLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Reset password +func NewTelephoneResetPasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *TelephoneResetPasswordLogic { + return &TelephoneResetPasswordLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.TelephoneResetPasswordRequest) (resp *types.LoginResponse, err error) { + code := req.Code + + phoneNumber, err := phone.FormatToE164(req.TelephoneAreaCode, req.Telephone) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number") + } + + if l.svcCtx.Config.Mobile.Enable { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SmsNotEnabled), "sms login is not enabled") + } + + // if the email verification is enabled, the verification code is required + cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.Security, phoneNumber) + value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() + if err != nil { + l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + + if value != code { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + + authMethods, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "mobile", phoneNumber) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorw("FindOneByTelephone Error", logger.Field("error", err)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error()) + } + if authMethods.UserId == 0 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user telephone exist: %v", phoneNumber) + } + + // Check if the user exists + userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, authMethods.UserId) + if err != nil { + l.Errorw("FindOneByTelephone Error", logger.Field("error", err)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error()) + } + + // Generate password + pwd := tool.EncodePassWord(req.Password) + userInfo.Password = pwd + err = l.svcCtx.UserModel.Update(l.ctx, userInfo) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "update user password failed: %v", err.Error()) + } + + // Generate session id + sessionId := uuidx.NewUUID().String() + // Generate token + token, err := jwt.NewJwtToken( + l.svcCtx.Config.JwtAuth.AccessSecret, + time.Now().Unix(), + l.svcCtx.Config.JwtAuth.AccessExpire, + jwt.WithOption("UserId", userInfo.Id), + jwt.WithOption("SessionId", sessionId), + ) + if err != nil { + l.Errorw("[UserLogin] token generate error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) + } + sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) + if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) + } + return &types.LoginResponse{ + Token: token, + }, nil +} diff --git a/internal/logic/auth/telephoneUserRegisterLogic.go b/internal/logic/auth/telephoneUserRegisterLogic.go new file mode 100644 index 0000000..24322b9 --- /dev/null +++ b/internal/logic/auth/telephoneUserRegisterLogic.go @@ -0,0 +1,182 @@ +package auth + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/jwt" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/phone" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/uuidx" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type CacheKeyPayload struct { + Code string `json:"code"` + LastAt int64 `json:"lastAt"` +} + +type TelephoneUserRegisterLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewTelephoneUserRegisterLogic User Telephone register +func NewTelephoneUserRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *TelephoneUserRegisterLogic { + return &TelephoneUserRegisterLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneRegisterRequest) (resp *types.LoginResponse, err error) { + c := l.svcCtx.Config.Register + // Check if the registration is stopped + if c.StopRegister { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.StopRegister), "stop register") + } + if !phone.Check(req.TelephoneAreaCode, req.Telephone) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "telephone number error") + } + + if !l.svcCtx.Config.Mobile.Enable { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SmsNotEnabled), "sms login is not enabled") + } + + phoneNumber, err := phone.FormatToE164(req.TelephoneAreaCode, req.Telephone) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number") + } + + // if the email verification is enabled, the verification code is required + cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.ParseVerifyType(uint8(constant.Register)), phoneNumber) + value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() + if err != nil { + l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + + var payload CacheKeyPayload + err = json.Unmarshal([]byte(value), &payload) + if err != nil { + l.Errorw("Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + if payload.Code != req.Code { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + l.svcCtx.Redis.Del(l.ctx, cacheKey) + // Check if the user exists + _, err = l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "mobile", phoneNumber) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorw("FindOneByTelephone Error", logger.Field("error", err)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error()) + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "telephone already exists") + } + var referer *user.User + if req.Invite == "" { + if l.svcCtx.Config.Invite.ForcedInvite { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InviteCodeError), "invite code is required") + } + } else { + // Check if the invite code is valid + referer, err = l.svcCtx.UserModel.FindOneByReferCode(l.ctx, req.Invite) + if err != nil { + l.Errorw("FindOneByReferCode Error", logger.Field("error", err)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InviteCodeError), "invite code is invalid") + } + } + + // Generate password + pwd := tool.EncodePassWord(req.Password) + userInfo := &user.User{ + Password: pwd, + AuthMethods: []user.AuthMethods{ + { + AuthType: "mobile", + AuthIdentifier: phoneNumber, + Verified: true, + }, + }, + } + if referer != nil { + userInfo.RefererId = referer.Id + } + err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + // Save user information + if err := db.Create(userInfo).Error; err != nil { + return err + } + // Generate ReferCode + userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id) + // Update ReferCode + if err := db.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil { + return err + } + if l.svcCtx.Config.Register.EnableTrial { + // Active trial + if err = l.activeTrial(userInfo.Id); err != nil { + return err + } + } + return nil + }) + // Generate session id + sessionId := uuidx.NewUUID().String() + // Generate token + token, err := jwt.NewJwtToken( + l.svcCtx.Config.JwtAuth.AccessSecret, + time.Now().Unix(), + l.svcCtx.Config.JwtAuth.AccessExpire, + jwt.WithOption("UserId", userInfo.Id), + jwt.WithOption("SessionId", sessionId), + ) + if err != nil { + l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) + } + sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) + if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) + } + return &types.LoginResponse{ + Token: token, + }, nil +} + +func (l *TelephoneUserRegisterLogic) activeTrial(uid int64) error { + sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe) + if err != nil { + return err + } + userSub := &user.Subscribe{ + Id: 0, + UserId: uid, + OrderId: 0, + SubscribeId: sub.Id, + StartTime: time.Now(), + ExpireTime: tool.AddTime(l.svcCtx.Config.Register.TrialTimeUnit, l.svcCtx.Config.Register.TrialTime, time.Now()), + Traffic: sub.Traffic, + Download: 0, + Upload: 0, + Token: uuidx.SubscribeToken(fmt.Sprintf("Trial-%v", uid)), + UUID: uuidx.NewUUID().String(), + Status: 1, + } + return l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub) +} diff --git a/internal/logic/auth/userLoginLogic.go b/internal/logic/auth/userLoginLogic.go new file mode 100644 index 0000000..ebc8ae9 --- /dev/null +++ b/internal/logic/auth/userLoginLogic.go @@ -0,0 +1,89 @@ +package auth + +import ( + "context" + "fmt" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/jwt" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/uuidx" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" +) + +type UserLoginLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewUserLoginLogic User login +func NewUserLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserLoginLogic { + return &UserLoginLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.LoginResponse, err error) { + loginStatus := false + var userInfo *user.User + // Record login status + defer func(svcCtx *svc.ServiceContext) { + if userInfo.Id != 0 { + if err := svcCtx.UserModel.InsertLoginLog(l.ctx, &user.LoginLog{ + UserId: userInfo.Id, + LoginIP: req.IP, + UserAgent: req.UserAgent, + Success: &loginStatus, + }); err != nil { + l.Logger.Error("[UserLogin] insert login log error", logger.Field("error", err.Error())) + } + } + }(l.svcCtx) + + userInfo, err = l.svcCtx.UserModel.FindOneByEmail(l.ctx, req.Email) + if err != nil { + if errors.As(err, &gorm.ErrRecordNotFound) { + logger.WithContext(l.ctx).Error(err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user email not exist: %v", req.Email) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error()) + } + // Verify password + if !tool.VerifyPassWord(req.Password, userInfo.Password) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password") + } + // Generate session id + sessionId := uuidx.NewUUID().String() + // Generate token + token, err := jwt.NewJwtToken( + l.svcCtx.Config.JwtAuth.AccessSecret, + time.Now().Unix(), + l.svcCtx.Config.JwtAuth.AccessExpire, + jwt.WithOption("UserId", userInfo.Id), + jwt.WithOption("SessionId", sessionId), + ) + if err != nil { + l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) + } + sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) + if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) + } + loginStatus = true + return &types.LoginResponse{ + Token: token, + }, nil +} diff --git a/internal/logic/auth/userRegisterLogic.go b/internal/logic/auth/userRegisterLogic.go new file mode 100644 index 0000000..b03c94d --- /dev/null +++ b/internal/logic/auth/userRegisterLogic.go @@ -0,0 +1,182 @@ +package auth + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/logic/common" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/jwt" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/uuidx" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type UserRegisterLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewUserRegisterLogic User register +func NewUserRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserRegisterLogic { + return &UserRegisterLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *types.LoginResponse, err error) { + + c := l.svcCtx.Config.Register + email := l.svcCtx.Config.Email + var referer *user.User + // Check if the registration is stopped + if c.StopRegister { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.StopRegister), "stop register") + } + + if req.Invite == "" { + if l.svcCtx.Config.Invite.ForcedInvite { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InviteCodeError), "invite code is required") + } + } else { + // Check if the invite code is valid + referer, err = l.svcCtx.UserModel.FindOneByReferCode(l.ctx, req.Invite) + if err != nil { + l.Errorw("FindOneByReferCode Error", logger.Field("error", err)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InviteCodeError), "invite code is invalid") + } + } + + // if the email verification is enabled, the verification code is required + if email.EnableVerify { + cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Register, req.Email) + value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() + if err != nil { + l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + var payload common.CacheKeyPayload + err = json.Unmarshal([]byte(value), &payload) + if err != nil { + l.Errorw("Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + if payload.Code != req.Code { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + } + // Check if the user exists + _, err = l.svcCtx.UserModel.FindOneByEmail(l.ctx, req.Email) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorw("FindOneByEmail Error", logger.Field("error", err)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error()) + } else if err == nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "user email exist: %v", req.Email) + } + // Generate password + pwd := tool.EncodePassWord(req.Password) + userInfo := &user.User{ + Password: pwd, + } + if referer != nil { + userInfo.RefererId = referer.Id + } + err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + // Save user information + if err := db.Create(userInfo).Error; err != nil { + return err + } + // Generate ReferCode + userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id) + // Update ReferCode + if err := db.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil { + return err + } + // create user auth info + authInfo := &user.AuthMethods{ + UserId: userInfo.Id, + AuthType: "email", + AuthIdentifier: req.Email, + Verified: email.EnableVerify, + } + if err = db.Create(authInfo).Error; err != nil { + return err + } + + if l.svcCtx.Config.Register.EnableTrial { + // Active trial + if err = l.activeTrial(userInfo.Id); err != nil { + return err + } + } + return nil + }) + // Generate session id + sessionId := uuidx.NewUUID().String() + // Generate token + token, err := jwt.NewJwtToken( + l.svcCtx.Config.JwtAuth.AccessSecret, + time.Now().Unix(), + l.svcCtx.Config.JwtAuth.AccessExpire, + jwt.WithOption("UserId", userInfo.Id), + jwt.WithOption("SessionId", sessionId), + ) + if err != nil { + l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) + } + // Set session id + sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) + if err := l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) + } + loginStatus := true + defer func() { + if token != "" && userInfo.Id != 0 { + if err := l.svcCtx.UserModel.InsertLoginLog(l.ctx, &user.LoginLog{ + UserId: userInfo.Id, + LoginIP: req.IP, + UserAgent: req.UserAgent, + Success: &loginStatus, + }); err != nil { + l.Logger.Error("[UserRegister] insert login log error", logger.Field("error", err.Error())) + } + } + }() + return &types.LoginResponse{ + Token: token, + }, nil +} + +func (l *UserRegisterLogic) activeTrial(uid int64) error { + sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe) + if err != nil { + return err + } + userSub := &user.Subscribe{ + UserId: uid, + OrderId: 0, + SubscribeId: sub.Id, + StartTime: time.Now(), + ExpireTime: tool.AddTime(l.svcCtx.Config.Register.TrialTimeUnit, l.svcCtx.Config.Register.TrialTime, time.Now()), + Traffic: sub.Traffic, + Download: 0, + Upload: 0, + Token: uuidx.SubscribeToken(fmt.Sprintf("Trial-%v", uid)), + UUID: uuidx.NewUUID().String(), + Status: 1, + } + return l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub) +} diff --git a/internal/logic/common/checkverificationcodelogic.go b/internal/logic/common/checkverificationcodelogic.go new file mode 100644 index 0000000..4aeb85b --- /dev/null +++ b/internal/logic/common/checkverificationcodelogic.go @@ -0,0 +1,70 @@ +package common + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/authmethod" + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/phone" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type CheckVerificationCodeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Check verification code +func NewCheckVerificationCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CheckVerificationCodeLogic { + return &CheckVerificationCodeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CheckVerificationCodeLogic) CheckVerificationCode(req *types.CheckVerificationCodeRequest) (resp *types.CheckVerificationCodeRespone, err error) { + resp = &types.CheckVerificationCodeRespone{} + if req.Method == authmethod.Email { + cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.ParseVerifyType(req.Type), req.Account) + value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() + if err != nil { + return resp, nil + } + var payload CacheKeyPayload + if err := json.Unmarshal([]byte(value), &payload); err != nil { + return resp, nil + } + if payload.Code != req.Code { + return resp, nil + } + resp.Status = true + } + if req.Method == authmethod.Mobile { + if !phone.CheckPhone(req.Account) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number") + } + cacheKey := fmt.Sprintf("%s:%s:+%s", config.AuthCodeTelephoneCacheKey, constant.ParseVerifyType(req.Type), req.Account) + value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() + if err != nil { + return resp, nil + } + var payload CacheKeyPayload + if err := json.Unmarshal([]byte(value), &payload); err != nil { + return resp, nil + } + if payload.Code != req.Code { + return resp, nil + } + resp.Status = true + } + return resp, nil +} diff --git a/internal/logic/common/getAdsLogic.go b/internal/logic/common/getAdsLogic.go new file mode 100644 index 0000000..124d935 --- /dev/null +++ b/internal/logic/common/getAdsLogic.go @@ -0,0 +1,42 @@ +package common + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/ads" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" +) + +type GetAdsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Ads +func NewGetAdsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAdsLogic { + return &GetAdsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAdsLogic) GetAds(req *types.GetAdsRequest) (resp *types.GetAdsResponse, err error) { + // todo: add ads position and device + status := 1 + _, data, err := l.svcCtx.AdsModel.GetAdsListByPage(l.ctx, 1, 200, ads.Filter{ + Status: &status, + }) + if err != nil { + return nil, err + } + resp = &types.GetAdsResponse{ + List: make([]types.Ads, len(data)), + } + tool.DeepCopy(&resp.List, data) + return +} diff --git a/internal/logic/common/getApplicationLogic.go b/internal/logic/common/getApplicationLogic.go new file mode 100644 index 0000000..4477b3b --- /dev/null +++ b/internal/logic/common/getApplicationLogic.go @@ -0,0 +1,136 @@ +package common + +import ( + "context" + "strings" + + "github.com/perfect-panel/ppanel-server/internal/model/application" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type GetApplicationLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Tos Content +func NewGetApplicationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetApplicationLogic { + return &GetApplicationLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetApplicationLogic) GetApplication() (resp *types.GetAppcationResponse, err error) { + resp = &types.GetAppcationResponse{} + + cfg, err := l.svcCtx.ApplicationModel.FindOneConfig(l.ctx, 1) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Logger.Error("[GetAppInfo] FindOneAppConfig error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetAppInfo FindOneAppConfig error: %v", err.Error()) + } + if err != nil { + resp.Config = types.ApplicationConfig{} + } else { + resp.Config = types.ApplicationConfig{ + AppId: cfg.AppId, + EncryptionKey: cfg.EncryptionKey, + EncryptionMethod: cfg.EncryptionMethod, + Domains: strings.Split(cfg.Domains, ";"), + StartupPicture: cfg.StartupPicture, + StartupPictureSkipTime: cfg.StartupPictureSkipTime, + } + } + + var applications []*application.Application + err = l.svcCtx.ApplicationModel.Transaction(l.ctx, func(tx *gorm.DB) (err error) { + return tx.Model(applications).Preload("ApplicationVersions").Find(&applications).Error + }) + if err != nil { + l.Errorw("[QueryApplicationConfig] get application error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get application error: %v", err.Error()) + } + + if len(applications) == 0 { + return resp, nil + } + + for _, app := range applications { + applicationResponse := types.ApplicationResponseInfo{ + Id: app.Id, + Name: app.Name, + Icon: app.Icon, + Description: app.Description, + SubscribeType: app.SubscribeType, + } + applicationVersions := app.ApplicationVersions + if len(applicationVersions) != 0 { + for _, applicationVersion := range applicationVersions { + /*if !applicationVersion.IsDefault { + continue + }*/ + switch applicationVersion.Platform { + case "ios": + applicationResponse.Platform.IOS = append(applicationResponse.Platform.IOS, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + case "macos": + applicationResponse.Platform.MacOS = append(applicationResponse.Platform.MacOS, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + case "linux": + applicationResponse.Platform.Linux = append(applicationResponse.Platform.Linux, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + case "android": + applicationResponse.Platform.Android = append(applicationResponse.Platform.Android, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + case "windows": + applicationResponse.Platform.Windows = append(applicationResponse.Platform.Windows, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + case "harmony": + applicationResponse.Platform.Harmony = append(applicationResponse.Platform.Harmony, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + } + } + } + resp.Applications = append(resp.Applications, applicationResponse) + } + + return +} diff --git a/internal/logic/common/getGlobalConfigLogic.go b/internal/logic/common/getGlobalConfigLogic.go new file mode 100644 index 0000000..b43d3ab --- /dev/null +++ b/internal/logic/common/getGlobalConfigLogic.go @@ -0,0 +1,82 @@ +package common + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetGlobalConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get global config +func NewGetGlobalConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetGlobalConfigLogic { + return &GetGlobalConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetGlobalConfigLogic) GetGlobalConfig() (resp *types.GetGlobalConfigResponse, err error) { + resp = new(types.GetGlobalConfigResponse) + + currencyCfg, err := l.svcCtx.SystemModel.GetCurrencyConfig(l.ctx) + if err != nil { + l.Logger.Error("[GetGlobalConfigLogic] GetCurrencyConfig error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetCurrencyConfig error: %v", err.Error()) + } + verifyCodeCfg, err := l.svcCtx.SystemModel.GetVerifyCodeConfig(l.ctx) + if err != nil { + l.Logger.Error("[GetGlobalConfigLogic] GetVerifyCodeConfig error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetVerifyCodeConfig error: %v", err.Error()) + } + + tool.DeepCopy(&resp.Site, l.svcCtx.Config.Site) + tool.DeepCopy(&resp.Subscribe, l.svcCtx.Config.Subscribe) + tool.DeepCopy(&resp.Auth.Email, l.svcCtx.Config.Email) + tool.DeepCopy(&resp.Auth.Mobile, l.svcCtx.Config.Mobile) + tool.DeepCopy(&resp.Auth.Register, l.svcCtx.Config.Register) + tool.DeepCopy(&resp.Verify, l.svcCtx.Config.Verify) + tool.DeepCopy(&resp.Invite, l.svcCtx.Config.Invite) + tool.SystemConfigSliceReflectToStruct(currencyCfg, &resp.Currency) + tool.SystemConfigSliceReflectToStruct(verifyCodeCfg, &resp.VerifyCode) + + resp.Verify = types.VeifyConfig{ + TurnstileSiteKey: l.svcCtx.Config.Verify.TurnstileSiteKey, + EnableLoginVerify: l.svcCtx.Config.Verify.LoginVerify, + EnableRegisterVerify: l.svcCtx.Config.Verify.RegisterVerify, + EnableResetPasswordVerify: l.svcCtx.Config.Verify.ResetPasswordVerify, + } + var methods []string + + // auth methods + authMethods, err := l.svcCtx.AuthModel.FindAll(l.ctx) + if err != nil { + l.Logger.Error("[GetGlobalConfigLogic] FindAll error: ", logger.Field("error", err.Error())) + } + + for _, method := range authMethods { + if *method.Enabled { + methods = append(methods, method.Method) + } + } + resp.OAuthMethods = methods + + webAds, err := l.svcCtx.SystemModel.FindOneByKey(l.ctx, "WebAD") + if err != nil { + l.Logger.Error("[GetGlobalConfigLogic] FindOneByKey error: ", logger.Field("error", err.Error()), logger.Field("key", "WebAD")) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneByKey error: %v", err.Error()) + } + // web ads config + resp.WebAd = webAds.Value == "true" + return +} diff --git a/internal/logic/common/getPrivacyPolicyLogic.go b/internal/logic/common/getPrivacyPolicyLogic.go new file mode 100644 index 0000000..ce6145f --- /dev/null +++ b/internal/logic/common/getPrivacyPolicyLogic.go @@ -0,0 +1,40 @@ +package common + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetPrivacyPolicyLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Privacy Policy +func NewGetPrivacyPolicyLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPrivacyPolicyLogic { + return &GetPrivacyPolicyLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetPrivacyPolicyLogic) GetPrivacyPolicy() (resp *types.PrivacyPolicyConfig, err error) { + resp = &types.PrivacyPolicyConfig{} + // get tos config from db + configs, err := l.svcCtx.SystemModel.GetTosConfig(l.ctx) + if err != nil { + l.Errorw("[GetTosConfig] GetTosConfig error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetTosConfig error: %v", err.Error()) + } + // reflect to response + tool.SystemConfigSliceReflectToStruct(configs, resp) + return +} diff --git a/internal/logic/common/getStatLogic.go b/internal/logic/common/getStatLogic.go new file mode 100644 index 0000000..9a4d604 --- /dev/null +++ b/internal/logic/common/getStatLogic.go @@ -0,0 +1,131 @@ +package common + +import ( + "context" + "encoding/json" + "io" + "net" + "net/http" + "slices" + "strings" + "time" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/model/server" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetStatLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Tos +func NewGetStatLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetStatLogic { + return &GetStatLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetStatLogic) GetStat() (resp *types.GetStatResponse, err error) { + respJson, err := l.svcCtx.Redis.Get(l.ctx, config.CommonStatCacheKey).Result() + if err == nil { + err = json.Unmarshal([]byte(respJson), resp) + if err == nil { + return + } + } + var u int64 + err = l.svcCtx.DB.Model(&user.User{}).Where("enable = 1").Count(&u).Error + if err != nil { + l.Logger.Error("[GetStatLogic] get user count failed: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get user count failed: %v", err.Error()) + } + if u > 100 { + u -= u % 100 + } else if u > 10 { + u -= u % 10 + } else { + u = 1 + } + var n int64 + err = l.svcCtx.DB.Model(&server.Server{}).Where("enable = 1").Count(&n).Error + if err != nil { + l.Logger.Error("[GetStatLogic] get server count failed: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get server count failed: %v", err.Error()) + } + var nodeaddr []string + err = l.svcCtx.DB.Model(&server.Server{}).Where("enable = 1").Pluck("server_addr", &nodeaddr).Error + if err != nil { + l.Logger.Error("[GetStatLogic] get server_addr failed: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get server_addr failed: %v", err.Error()) + } + type apireq struct { + query string + fields string + } + type apiret struct { + CountryCode string `json:"countryCode"` + } + //map as dict + type void struct{} + var v void + country := make(map[string]void) + for c := range slices.Chunk(nodeaddr, 100) { + var batchreq []apireq + for _, addr := range c { + isAddr := net.ParseIP(addr) + if isAddr == nil { + ip, err := net.LookupIP(addr) + if err == nil && len(ip) > 0 { + batchreq = append(batchreq, apireq{query: ip[0].String(), fields: "countryCode"}) + } + } else { + batchreq = append(batchreq, apireq{query: addr, fields: "countryCode"}) + } + } + req, _ := json.Marshal(batchreq) + ret, err := http.Post("http://ip-api.com/batch", "application/json", strings.NewReader(string(req))) + if err == nil { + retBytes, err := io.ReadAll(ret.Body) + if err == nil { + var retStruct []apiret + err := json.Unmarshal(retBytes, &retStruct) + if err == nil { + for _, dat := range retStruct { + if dat.CountryCode != "" { + country[dat.CountryCode] = v + } + } + } + } + } + } + protocolDict := make(map[string]void) + var protocol []string + l.svcCtx.DB.Model(&server.Server{}).Where("enable = true").Pluck("protocol", &protocol) + for _, p := range protocol { + protocolDict[p] = v + } + protocol = nil + for p := range protocolDict { + protocol = append(protocol, p) + } + resp = &types.GetStatResponse{ + User: u, + Node: n, + Country: int64(len(country)), + Protocol: protocol, + } + val, _ := json.Marshal(*resp) + _ = l.svcCtx.Redis.Set(l.ctx, config.CommonStatCacheKey, string(val), time.Duration(3600)*time.Second).Err() + return resp, nil +} diff --git a/internal/logic/common/getSubscriptionLogic.go b/internal/logic/common/getSubscriptionLogic.go new file mode 100644 index 0000000..bd5677e --- /dev/null +++ b/internal/logic/common/getSubscriptionLogic.go @@ -0,0 +1,41 @@ +package common + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetSubscriptionLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Subscription +func NewGetSubscriptionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSubscriptionLogic { + return &GetSubscriptionLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetSubscriptionLogic) GetSubscription() (resp *types.GetSubscriptionResponse, err error) { + resp = &types.GetSubscriptionResponse{ + List: make([]types.Subscribe, 0), + } + // Get the subscription list + data, err := l.svcCtx.SubscribeModel.QuerySubscribeListByShow(l.ctx) + if err != nil { + l.Errorw("[Site GetSubscription]", logger.Field("err", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get subscription list error: %v", err.Error()) + } + tool.DeepCopy(&resp.List, data) + return +} diff --git a/internal/logic/common/getTosLogic.go b/internal/logic/common/getTosLogic.go new file mode 100644 index 0000000..8884bc4 --- /dev/null +++ b/internal/logic/common/getTosLogic.go @@ -0,0 +1,40 @@ +package common + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetTosLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Tos +func NewGetTosLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetTosLogic { + return &GetTosLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetTosLogic) GetTos() (resp *types.GetTosResponse, err error) { + resp = &types.GetTosResponse{} + // get Tos config from db + configs, err := l.svcCtx.SystemModel.GetTosConfig(l.ctx) + if err != nil { + l.Errorw("[GetTosLogic] GetTos error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetTos error: %v", err.Error()) + } + // reflect to response + tool.SystemConfigSliceReflectToStruct(configs, resp) + return +} diff --git a/internal/logic/common/sendEmailCodeLogic.go b/internal/logic/common/sendEmailCodeLogic.go new file mode 100644 index 0000000..467f977 --- /dev/null +++ b/internal/logic/common/sendEmailCodeLogic.go @@ -0,0 +1,156 @@ +package common + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "text/template" + "time" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/limit" + "github.com/perfect-panel/ppanel-server/pkg/random" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + queue "github.com/perfect-panel/ppanel-server/queue/types" +) + +type SendEmailCodeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +const ( + IntervalTime = 60 +) + +type VerifyTemplate struct { + Type uint8 + SiteLogo string + SiteName string + Expire uint8 + Code string +} +type CacheKeyPayload struct { + Code string `json:"code"` + LastAt int64 `json:"lastAt"` +} + +// NewSendEmailCodeLogic Get verification code +func NewSendEmailCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendEmailCodeLogic { + return &SendEmailCodeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SendEmailCodeLogic) SendEmailCode(req *types.SendCodeRequest) (resp *types.SendCodeResponse, err error) { + // Check if there is Redis in the code + cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.ParseVerifyType(req.Type), req.Email) + // Check if the limit is exceeded of current request + limiter := limit.NewPeriodLimit(60, 1, l.svcCtx.Redis, fmt.Sprintf("%s:%s:%s", config.SendIntervalKeyPrefix, "email", constant.ParseVerifyType(req.Type))) + permit, err := limiter.Take(req.Email) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to take limit") + } + if !limiter.ParsePermitState(permit) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "send email too many requests") + } + // Check if the limit is exceeded of today + permit, err = l.svcCtx.AuthLimiter.Take(fmt.Sprintf("%s:%s:%s", "email", constant.ParseVerifyType(req.Type), req.Email)) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to take limit") + } + if !l.svcCtx.AuthLimiter.ParsePermitState(permit) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.TodaySendCountExceedsLimit), "send email too many requests") + } + m, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "email", req.Email) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindUserAuthMethodByOpenID error") + } + if constant.ParseVerifyType(req.Type) == constant.Register && m.Id > 0 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "mobile already bind") + } else if constant.ParseVerifyType(req.Type) == constant.Security && m.Id == 0 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "mobile not bind") + } + + var payload CacheKeyPayload + var taskPayload queue.SendEmailPayload + // Generate verification code + code := random.Key(6, 0) + taskPayload.Email = req.Email + taskPayload.Subject = "Verification code" + content, err := l.initTemplate(req.Type, code) + if err != nil { + l.Logger.Error("[SendEmailCode]: InitTemplate Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to init template") + } + taskPayload.Content = content + // Save to Redis + payload = CacheKeyPayload{ + Code: code, + LastAt: time.Now().Unix(), + } + // Marshal the payload + val, _ := json.Marshal(payload) + if err = l.svcCtx.Redis.Set(l.ctx, cacheKey, string(val), time.Second*IntervalTime*5).Err(); err != nil { + l.Errorw("[SendEmailCode]: Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) + return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to set verification code") + } + + // Marshal the task payload + payloadBuy, err := json.Marshal(taskPayload) + if err != nil { + l.Errorw("[SendEmailCode]: Marshal Error", logger.Field("error", err.Error())) + return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to marshal task payload") + } + // Create a queue task + task := asynq.NewTask(queue.ForthwithSendEmail, payloadBuy, asynq.MaxRetry(3)) + // Enqueue the task + taskInfo, err := l.svcCtx.Queue.Enqueue(task) + if err != nil { + l.Errorw("[SendEmailCode]: Enqueue Error", logger.Field("error", err.Error()), logger.Field("payload", string(payloadBuy))) + return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to enqueue task") + } + l.Infow("[SendEmailCode]: Enqueue Success", logger.Field("taskID", taskInfo.ID), logger.Field("payload", string(payloadBuy))) + if l.svcCtx.Config.Model == constant.DevMode { + return &types.SendCodeResponse{ + Code: payload.Code, + Status: true, + }, nil + } else { + return &types.SendCodeResponse{ + Status: true, + }, nil + } +} + +func (l *SendEmailCodeLogic) initTemplate(t uint8, code string) (string, error) { + data := VerifyTemplate{ + Type: t, + SiteLogo: l.svcCtx.Config.Site.SiteLogo, + SiteName: l.svcCtx.Config.Site.SiteName, + Expire: 5, + Code: code, + } + tpl, err := template.New("verify").Parse(l.svcCtx.Config.Email.VerifyEmailTemplate) + if err != nil { + return "", err + } + var result bytes.Buffer + err = tpl.Execute(&result, data) + if err != nil { + return "", err + } + return result.String(), nil +} diff --git a/internal/logic/common/sendSmsCodeLogic.go b/internal/logic/common/sendSmsCodeLogic.go new file mode 100644 index 0000000..ada3f8d --- /dev/null +++ b/internal/logic/common/sendSmsCodeLogic.go @@ -0,0 +1,123 @@ +package common + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/limit" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/phone" + "github.com/perfect-panel/ppanel-server/pkg/random" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + queue "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type SmsSendCount struct { + Count int64 `json:"count"` + CreateAt int64 `json:"create_at"` +} + +type SendSmsCodeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewSendSmsCodeLogic Get sms verification code +func NewSendSmsCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendSmsCodeLogic { + return &SendSmsCodeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SendSmsCodeLogic) SendSmsCode(req *types.SendSmsCodeRequest) (resp *types.SendCodeResponse, err error) { + phoneNumber, err := phone.FormatToE164(req.TelephoneAreaCode, req.Telephone) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number") + } + + cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.ParseVerifyType(req.Type), phoneNumber) + // Check if the limit is exceeded of current request + limiter := limit.NewPeriodLimit(60, 1, l.svcCtx.Redis, fmt.Sprintf("%s:%s:%s", config.SendIntervalKeyPrefix, "mobile", constant.ParseVerifyType(req.Type))) + permit, err := limiter.Take(phoneNumber) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to take limit") + } + if !limiter.ParsePermitState(permit) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "send sms too many requests") + } + // Check if the limit is exceeded of the today + permit, err = l.svcCtx.AuthLimiter.Take(fmt.Sprintf("%s:%s:%s", "mobile", constant.ParseVerifyType(req.Type), phoneNumber)) + if err != nil { + return nil, err + } + if !l.svcCtx.AuthLimiter.ParsePermitState(permit) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.TodaySendCountExceedsLimit), "This account has reached the limit of sending times today") + } + m, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "mobile", phoneNumber) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindUserAuthMethodByOpenID error") + } + if constant.ParseVerifyType(req.Type) == constant.Register && m.Id > 0 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "mobile already bind") + } else if constant.ParseVerifyType(req.Type) == constant.Security && m.Id == 0 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "mobile not bind") + } + + taskPayload := queue.SendSmsPayload{ + Type: req.Type, + Telephone: req.Telephone, + TelephoneArea: req.TelephoneAreaCode, + } + // Generate verification code + code := random.Key(6, 0) + taskPayload.Telephone = req.Telephone + taskPayload.Content = code + // Save to Redis + payload := CacheKeyPayload{ + Code: code, + LastAt: time.Now().Unix(), + } + // Marshal the payload + val, _ := json.Marshal(payload) + if err = l.svcCtx.Redis.Set(l.ctx, cacheKey, string(val), time.Second*time.Duration(l.svcCtx.Config.VerifyCode.ExpireTime)).Err(); err != nil { + l.Errorw("[SendSmsCode]: Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) + return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to set verification code") + } + + // Marshal the task payload + payloadValue, err := json.Marshal(taskPayload) + if err != nil { + l.Errorw("[SendSmsCode]: Marshal Error", logger.Field("error", err.Error())) + return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to marshal task payload") + } + // Create a queue task + task := asynq.NewTask(queue.ForthwithSendSms, payloadValue) + // Enqueue the task + taskInfo, err := l.svcCtx.Queue.Enqueue(task) + if err != nil { + l.Errorw("[SendSmsCode]: Enqueue Error", logger.Field("error", err.Error()), logger.Field("payload", string(payloadValue))) + return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to enqueue task") + } + l.Infow("[SendSmsCode]: Enqueue Success", logger.Field("taskID", taskInfo.ID), logger.Field("payload", string(payloadValue))) + if l.svcCtx.Config.Model == constant.DevMode { + return &types.SendCodeResponse{ + Code: taskPayload.Content, + Status: true, + }, nil + } + return &types.SendCodeResponse{ + Status: true, + }, nil +} diff --git a/internal/logic/notify/alipayNotifyLogic.go b/internal/logic/notify/alipayNotifyLogic.go new file mode 100644 index 0000000..125e41f --- /dev/null +++ b/internal/logic/notify/alipayNotifyLogic.go @@ -0,0 +1,96 @@ +package notify + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/model/payment" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/payment/alipay" + "github.com/perfect-panel/ppanel-server/queue/types" +) + +type AlipayNotifyLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Alipay notify +func NewAlipayNotifyLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AlipayNotifyLogic { + return &AlipayNotifyLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AlipayNotifyLogic) AlipayNotify(r *http.Request) error { + data, ok := l.ctx.Value(constant.CtxKeyPayment).(*payment.Payment) + if !ok { + return fmt.Errorf("payment config not found") + } + var config payment.AlipayF2FConfig + if err := json.Unmarshal([]byte(data.Config), &config); err != nil { + l.Logger.Error("[AlipayNotify] Unmarshal config failed", logger.Field("error", err.Error())) + return err + } + client := alipay.NewClient(alipay.Config{ + AppId: config.AppId, + PrivateKey: config.PrivateKey, + PublicKey: config.PublicKey, + InvoiceName: config.InvoiceName, + NotifyURL: data.Domain + "/v1/payment/alipay/notify", + }) + notify, err := client.DecodeNotification(r.Form) + if err != nil { + l.Logger.Error("[AlipayNotify] Decode notification failed", logger.Field("error", err.Error())) + return err + } + if notify.Status == alipay.Success { + orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, notify.OrderNo) + if err != nil { + l.Logger.Error("[AlipayNotify] Find order failed", logger.Field("error", err.Error()), logger.Field("orderNo", notify.OrderNo)) + return errors.Wrapf(xerr.NewErrCode(xerr.OrderNotExist), "order not exist: %v", notify.OrderNo) + } + + if orderInfo.Status == 5 { + return nil + } + + // Update order status + err = l.svcCtx.OrderModel.UpdateOrderStatus(l.ctx, notify.OrderNo, 2) + if err != nil { + l.Logger.Error("[AlipayNotify] Update order status failed", logger.Field("error", err.Error()), logger.Field("orderNo", notify.OrderNo)) + return err + } + l.Logger.Info("[AlipayNotify] Notify status success", logger.Field("orderNo", notify.OrderNo)) + payload := types.ForthwithActivateOrderPayload{ + OrderNo: notify.OrderNo, + } + bytes, err := json.Marshal(&payload) + if err != nil { + l.Logger.Error("[AlipayNotify] Marshal payload failed", logger.Field("error", err.Error())) + return err + } + task := asynq.NewTask(types.ForthwithActivateOrder, bytes) + taskInfo, err := l.svcCtx.Queue.EnqueueContext(l.ctx, task) + if err != nil { + l.Logger.Error("[AlipayNotify] Enqueue task failed", logger.Field("error", err.Error())) + return err + } + l.Logger.Info("[AlipayNotify] Enqueue task success", logger.Field("taskInfo", taskInfo)) + } else { + l.Logger.Error("[AlipayNotify] Notify status failed", logger.Field("status", string(notify.Status))) + } + return nil +} diff --git a/internal/logic/notify/ePayNotifyLogic.go b/internal/logic/notify/ePayNotifyLogic.go new file mode 100644 index 0000000..385ce1b --- /dev/null +++ b/internal/logic/notify/ePayNotifyLogic.go @@ -0,0 +1,106 @@ +package notify + +import ( + "encoding/json" + "net/url" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/gin-gonic/gin" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/model/payment" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/payment/epay" + + queueType "github.com/perfect-panel/ppanel-server/queue/types" +) + +type EPayNotifyLogic struct { + logger.Logger + ctx *gin.Context + svcCtx *svc.ServiceContext +} + +// EPay notify +func NewEPayNotifyLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *EPayNotifyLogic { + return &EPayNotifyLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error { + + // Find payment config + data, ok := l.ctx.Request.Context().Value(constant.CtxKeyPayment).(*payment.Payment) + if !ok { + l.Logger.Error("[EPayNotify] Payment not found in context") + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "payment config not found") + } + l.Infof("[EPayNotify] Payment config: %+v", data) + orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OutTradeNo) + if err != nil { + l.Logger.Error("[EPayNotify] Find order failed", logger.Field("error", err.Error()), logger.Field("orderNo", req.OutTradeNo)) + return errors.Wrapf(xerr.NewErrCode(xerr.OrderNotExist), "order not exist: %v", req.OutTradeNo) + } + + var config payment.EPayConfig + if err := json.Unmarshal([]byte(data.Config), &config); err != nil { + l.Logger.Errorw("[EPayNotify] Unmarshal config failed", logger.Field("error", err.Error())) + return err + } + // Verify sign + client := epay.NewClient(config.Pid, config.Url, config.Key) + if !client.VerifySign(urlParamsToMap(l.ctx.Request.URL.RawQuery)) && !l.svcCtx.Config.Debug { + l.Logger.Error("[EPayNotify] Verify sign failed") + return nil + } + if req.TradeStatus != "TRADE_SUCCESS" { + l.Logger.Error("[EPayNotify] Trade status is not success", logger.Field("orderNo", req.OutTradeNo), logger.Field("tradeStatus", req.TradeStatus)) + return nil + } + if orderInfo.Status == 5 { + return nil + } + // Update order status + err = l.svcCtx.OrderModel.UpdateOrderStatus(l.ctx, req.OutTradeNo, 2) + if err != nil { + l.Logger.Error("[EPayNotify] Update order status failed", logger.Field("error", err.Error()), logger.Field("orderNo", req.OutTradeNo)) + return err + } + // Create activate order task + payload := queueType.ForthwithActivateOrderPayload{ + OrderNo: req.OutTradeNo, + } + bytes, err := json.Marshal(&payload) + if err != nil { + l.Logger.Error("[EPayNotify] Marshal payload failed", logger.Field("error", err.Error())) + return err + } + task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes) + taskInfo, err := l.svcCtx.Queue.EnqueueContext(l.ctx, task) + if err != nil { + l.Logger.Error("[EPayNotify] Enqueue task failed", logger.Field("error", err.Error())) + return err + } + l.Logger.Info("[EPayNotify] Enqueue task success", logger.Field("taskInfo", taskInfo)) + return nil +} + +func urlParamsToMap(query string) map[string]string { + params := make(map[string]string) + values, _ := url.ParseQuery(query) + for k, v := range values { + if len(v) > 0 { + params[k] = v[0] + } + } + return params +} diff --git a/internal/logic/notify/stripeNotifyLogic.go b/internal/logic/notify/stripeNotifyLogic.go new file mode 100644 index 0000000..a2c7e3a --- /dev/null +++ b/internal/logic/notify/stripeNotifyLogic.go @@ -0,0 +1,97 @@ +package notify + +import ( + "context" + "encoding/json" + "io" + "net/http" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/model/payment" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/payment/stripe" + "github.com/perfect-panel/ppanel-server/queue/types" +) + +type StripeNotifyLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewStripeNotifyLogic Stripe notify +func NewStripeNotifyLogic(ctx context.Context, svcCtx *svc.ServiceContext) *StripeNotifyLogic { + return &StripeNotifyLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *StripeNotifyLogic) StripeNotify(r *http.Request, w http.ResponseWriter) error { + const MaxBodyBytes = int64(65536) + r.Body = http.MaxBytesReader(w, r.Body, MaxBodyBytes) + payload, err := io.ReadAll(r.Body) + if err != nil { + l.Errorw("[StripeNotify] error", logger.Field("errors", err.Error())) + return err + } + signature := r.Header.Get("Stripe-Signature") + stripeConfig, ok := l.ctx.Value(constant.CtxKeyPayment).(*payment.Payment) + if !ok { + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "payment config not found") + } + config := payment.StripeConfig{} + if err := json.Unmarshal([]byte(stripeConfig.Config), &config); err != nil { + return err + } + client := stripe.NewClient(stripe.Config{ + PublicKey: config.PublicKey, + SecretKey: config.SecretKey, + WebhookSecret: config.WebhookSecret, + }) + + notify, err := client.ParseNotify(payload, signature) + if err != nil { + l.Errorw("[StripeNotify] error", logger.Field("errors", err.Error())) + return err + } + orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, notify.OrderNo) + if err != nil { + l.Logger.Error("[StripeNotify] Find order failed", logger.Field("error", err.Error()), logger.Field("orderNo", notify.OrderNo)) + return errors.Wrapf(xerr.NewErrCode(xerr.OrderNotExist), "order not exist: %v", notify.OrderNo) + } + if notify.EventType == "payment_intent.succeeded" { + if orderInfo.Status == 5 { + return nil + } + // update order status + err = l.svcCtx.OrderModel.UpdateOrderStatus(l.ctx, notify.OrderNo, 2) + if err != nil { + return err + } + // create ActivateOrder task + payload := types.ForthwithActivateOrderPayload{ + OrderNo: notify.OrderNo, + } + bytes, err := json.Marshal(payload) + if err != nil { + l.Errorw("[StripeNotify] Marshal error", logger.Field("errors", err.Error()), logger.Field("payload", payload)) + return err + } + task := asynq.NewTask(types.ForthwithActivateOrder, bytes) + _, err = l.svcCtx.Queue.Enqueue(task) + if err != nil { + l.Errorw("[StripeNotify] Enqueue error", logger.Field("errors", err.Error())) + return err + } + l.Infow("[StripeNotify] success", logger.Field("orderNo", notify.OrderNo)) + } + return nil +} diff --git a/internal/logic/public/announcement/queryAnnouncementLogic.go b/internal/logic/public/announcement/queryAnnouncementLogic.go new file mode 100644 index 0000000..1f9b71a --- /dev/null +++ b/internal/logic/public/announcement/queryAnnouncementLogic.go @@ -0,0 +1,46 @@ +package announcement + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/announcement" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type QueryAnnouncementLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Query announcement +func NewQueryAnnouncementLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryAnnouncementLogic { + return &QueryAnnouncementLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryAnnouncementLogic) QueryAnnouncement(req *types.QueryAnnouncementRequest) (resp *types.QueryAnnouncementResponse, err error) { + enable := true + total, list, err := l.svcCtx.AnnouncementModel.GetAnnouncementListByPage(l.ctx, req.Page, req.Size, announcement.Filter{ + Show: &enable, + Pinned: req.Pinned, + Popup: req.Popup, + }) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetAnnouncementListByPage error: %v", err.Error()) + } + resp = &types.QueryAnnouncementResponse{} + resp.Total = total + resp.List = make([]types.Announcement, 0) + tool.DeepCopy(&resp.List, list) + return +} diff --git a/internal/logic/public/document/queryDocumentDetailLogic.go b/internal/logic/public/document/queryDocumentDetailLogic.go new file mode 100644 index 0000000..fbeae76 --- /dev/null +++ b/internal/logic/public/document/queryDocumentDetailLogic.go @@ -0,0 +1,39 @@ +package document + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type QueryDocumentDetailLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get document detail +func NewQueryDocumentDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryDocumentDetailLogic { + return &QueryDocumentDetailLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryDocumentDetailLogic) QueryDocumentDetail(req *types.QueryDocumentDetailRequest) (resp *types.Document, err error) { + // find document + data, err := l.svcCtx.DocumentModel.FindOne(l.ctx, req.Id) + if err != nil { + l.Errorw("[QueryDocumentDetailLogic] FindOne error", logger.Field("id", req.Id), logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOne error: %s", err.Error()) + } + resp = &types.Document{} + tool.DeepCopy(resp, data) + return +} diff --git a/internal/logic/public/document/queryDocumentListLogic.go b/internal/logic/public/document/queryDocumentListLogic.go new file mode 100644 index 0000000..bf386ad --- /dev/null +++ b/internal/logic/public/document/queryDocumentListLogic.go @@ -0,0 +1,48 @@ +package document + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type QueryDocumentListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get document list +func NewQueryDocumentListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryDocumentListLogic { + return &QueryDocumentListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryDocumentListLogic) QueryDocumentList() (resp *types.QueryDocumentListResponse, err error) { + total, data, err := l.svcCtx.DocumentModel.GetDocumentListByAll(l.ctx) + if err != nil { + l.Errorw("[QueryDocumentList] error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryDocumentList error: %v", err.Error()) + } + resp = &types.QueryDocumentListResponse{ + Total: total, + List: make([]types.Document, 0), + } + for _, item := range data { + resp.List = append(resp.List, types.Document{ + Id: item.Id, + Title: item.Title, + Tags: tool.StringMergeAndRemoveDuplicates(item.Tags), + UpdatedAt: item.UpdatedAt.UnixMilli(), + }) + } + return +} diff --git a/internal/logic/public/order/calculateCoupon.go b/internal/logic/public/order/calculateCoupon.go new file mode 100644 index 0000000..e9150cb --- /dev/null +++ b/internal/logic/public/order/calculateCoupon.go @@ -0,0 +1,13 @@ +package order + +import ( + "github.com/perfect-panel/ppanel-server/internal/model/coupon" +) + +func calculateCoupon(amount int64, couponInfo *coupon.Coupon) int64 { + if couponInfo.Type == 1 { + return int64(float64(amount) * (float64(couponInfo.Discount) / float64(100))) + } else { + return min(couponInfo.Discount, amount) + } +} diff --git a/internal/logic/public/order/calculateFee.go b/internal/logic/public/order/calculateFee.go new file mode 100644 index 0000000..432ad27 --- /dev/null +++ b/internal/logic/public/order/calculateFee.go @@ -0,0 +1,20 @@ +package order + +import "github.com/perfect-panel/ppanel-server/internal/model/payment" + +func calculateFee(amount int64, config *payment.Payment) int64 { + var fee float64 + switch config.FeeMode { + case 0: + return 0 + case 1: + fee = float64(amount) * (float64(config.FeePercent) / float64(100)) + case 2: + if amount > 0 { + fee = float64(config.FeeAmount) + } + case 3: + fee = float64(amount)*(float64(config.FeePercent)/float64(100)) + float64(config.FeeAmount) + } + return int64(fee) +} diff --git a/internal/logic/public/order/closeOrderLogic.go b/internal/logic/public/order/closeOrderLogic.go new file mode 100644 index 0000000..3ecfff3 --- /dev/null +++ b/internal/logic/public/order/closeOrderLogic.go @@ -0,0 +1,219 @@ +package order + +import ( + "context" + "encoding/json" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/payment/payssion" + "github.com/perfect-panel/ppanel-server/pkg/payment/stripe" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/model/order" + "github.com/perfect-panel/ppanel-server/internal/model/payment" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/payment/alipay" +) + +type CloseOrderLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewCloseOrderLogic Close order +func NewCloseOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CloseOrderLogic { + return &CloseOrderLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CloseOrderLogic) CloseOrder(req *types.CloseOrderRequest) error { + // Find order information by order number + orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo) + if err != nil { + l.Errorw("[CloseOrder] Find order info failed", + logger.Field("error", err.Error()), + logger.Field("orderNo", req.OrderNo), + ) + return nil + } + // If the order status is not 1, it means that the order has been closed or paid + if orderInfo.Status != 1 { + l.Infow("[CloseOrder] Order status is not 1", + logger.Field("orderNo", req.OrderNo), + logger.Field("status", orderInfo.Status), + ) + return nil + } + err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { + // update order status + err := tx.Model(&order.Order{}).Where("order_no = ?", req.OrderNo).Update("status", 3).Error + if err != nil { + l.Errorw("[CloseOrder] Update order status failed", + logger.Field("error", err.Error()), + logger.Field("orderNo", req.OrderNo), + ) + return err + } + // If User ID is 0, it means that the order is a guest order and does not need to be refunded, the order can be deleted directly + if orderInfo.UserId == 0 { + err = tx.Model(&order.Order{}).Where("order_no = ?", req.OrderNo).Delete(&order.Order{}).Error + if err != nil { + l.Errorw("[CloseOrder] Delete order failed", + logger.Field("error", err.Error()), + logger.Field("orderNo", req.OrderNo), + ) + return err + } + return nil + } + // refund deduction amount to user deduction balance + if orderInfo.GiftAmount > 0 { + userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, orderInfo.UserId) + if err != nil { + l.Errorw("[CloseOrder] Find user info failed", + logger.Field("error", err.Error()), + logger.Field("user_id", orderInfo.UserId), + ) + return err + } + deduction := userInfo.GiftAmount + orderInfo.GiftAmount + err = tx.Model(&user.User{}).Where("id = ?", orderInfo.UserId).Update("deduction", deduction).Error + if err != nil { + l.Errorw("[CloseOrder] Refund deduction amount failed", + logger.Field("error", err.Error()), + logger.Field("uid", orderInfo.UserId), + logger.Field("deduction", orderInfo.GiftAmount), + ) + return err + } + // Record the deduction refund log + giftAmountLog := &user.GiftAmountLog{ + UserId: orderInfo.UserId, + OrderNo: orderInfo.OrderNo, + Amount: orderInfo.GiftAmount, + Type: 1, + Balance: deduction, + Remark: "Order cancellation refund", + } + err = tx.Model(&user.GiftAmountLog{}).Create(giftAmountLog).Error + if err != nil { + l.Errorw("[CloseOrder] Record cancellation refund log failed", + logger.Field("error", err.Error()), + logger.Field("uid", orderInfo.UserId), + logger.Field("deduction", orderInfo.GiftAmount), + ) + return err + } + // update user cache + return l.svcCtx.UserModel.UpdateUserCache(l.ctx, userInfo) + } + return nil + }) + if err != nil { + return err + } + return nil +} + +// confirmationPayment Determine whether the payment is successful +// +//nolint:unused +func (l *CloseOrderLogic) confirmationPayment(order *order.Order) bool { + paymentConfig, err := l.svcCtx.PaymentModel.FindOne(l.ctx, order.PaymentId) + if err != nil { + l.Errorw("[CloseOrder] Find payment config failed", logger.Field("error", err.Error()), logger.Field("paymentMark", order.Method)) + return false + } + switch order.Method { + case AlipayF2f: + if l.queryAlipay(paymentConfig, order.TradeNo) { + return true + } + case Payssion: + if l.queryPayssion(paymentConfig, order.TradeNo) { + return true + } + case StripeWeChatPay: + if l.queryStripe(paymentConfig, order.TradeNo) { + return true + } + default: + l.Infow("[CloseOrder] Unsupported payment method", logger.Field("paymentMethod", order.Method)) + } + return false +} + +// queryAlipay Query Alipay payment status +// +//nolint:unused +func (l *CloseOrderLogic) queryAlipay(paymentConfig *payment.Payment, TradeNo string) bool { + config := payment.AlipayF2FConfig{} + if err := json.Unmarshal([]byte(paymentConfig.Config), &config); err != nil { + l.Errorw("[CloseOrder] Unmarshal payment config failed", logger.Field("error", err.Error()), logger.Field("config", paymentConfig.Config)) + return false + } + client := alipay.NewClient(alipay.Config{ + AppId: config.AppId, + PrivateKey: config.PrivateKey, + PublicKey: config.PublicKey, + InvoiceName: config.InvoiceName, + }) + status, err := client.QueryTrade(l.ctx, TradeNo) + if err != nil { + l.Errorw("[CloseOrder] Query trade failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo)) + return false + } + if status == alipay.Success || status == alipay.Finished { + return true + } + return false +} + +// queryStripe Query Stripe payment status +// +//nolint:unused +func (l *CloseOrderLogic) queryStripe(paymentConfig *payment.Payment, TradeNo string) bool { + config := payment.StripeConfig{} + if err := json.Unmarshal([]byte(paymentConfig.Config), &config); err != nil { + l.Errorw("[CloseOrder] Unmarshal payment config failed", logger.Field("error", err.Error()), logger.Field("config", paymentConfig.Config)) + return false + } + client := stripe.NewClient(stripe.Config{ + PublicKey: config.PublicKey, + SecretKey: config.SecretKey, + WebhookSecret: config.WebhookSecret, + }) + status, err := client.QueryOrderStatus(TradeNo) + if err != nil { + l.Errorw("[CloseOrder] Query order status failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo)) + return false + } + return status +} + +// queryPayssion Query Stripe payment status +// +//nolint:unused +func (l *CloseOrderLogic) queryPayssion(paymentConfig *payment.Payment, TradeNo string) bool { + l.Infof("[CloseOrder]1 Query Payssion called") + payssionConfig := payment.PayssionConfig{} + if err := json.Unmarshal([]byte(paymentConfig.Config), &payssionConfig); err != nil { + l.Errorw("[CloseOrder] Unmarshal error", logger.Field("error", err.Error())) + return false + } + l.Infof("[CloseOrder]2 Query Payssion called") + client := payssion.NewClient(payssionConfig.ApiKey, payssionConfig.SecretKey, payssionConfig.PmId, payssionConfig.Currency, payssionConfig.QueryUrl, payssionConfig.CreateUrl) + // create payment + result, err := client.QueryOrder(TradeNo) + if err != nil { + l.Errorw("[CloseOrder] Query order status failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo)) + return false + } + l.Infof("[CloseOrder]3 Query Payssion called") + return result.Transaction.State == "completed" +} diff --git a/internal/logic/public/order/constant.go b/internal/logic/public/order/constant.go new file mode 100644 index 0000000..44bccd8 --- /dev/null +++ b/internal/logic/public/order/constant.go @@ -0,0 +1,10 @@ +package order + +const ( + Epay = "epay" + Payssion = "Payssion" + AlipayF2f = "alipay_f2f" + StripeAlipay = "stripe_alipay" + StripeWeChatPay = "stripe_wechat_pay" + Balance = "balance" +) diff --git a/internal/logic/public/order/getDiscount.go b/internal/logic/public/order/getDiscount.go new file mode 100644 index 0000000..4d896f9 --- /dev/null +++ b/internal/logic/public/order/getDiscount.go @@ -0,0 +1,14 @@ +package order + +import "github.com/perfect-panel/ppanel-server/internal/types" + +func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 { + var finalDiscount int64 = 100 + + for _, discount := range discounts { + if inputMonths >= discount.Quantity && discount.Discount < finalDiscount { + finalDiscount = discount.Discount + } + } + return float64(finalDiscount) / float64(100) +} diff --git a/internal/logic/public/order/preCreateOrderLogic.go b/internal/logic/public/order/preCreateOrderLogic.go new file mode 100644 index 0000000..2989cde --- /dev/null +++ b/internal/logic/public/order/preCreateOrderLogic.go @@ -0,0 +1,105 @@ +package order + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type PreCreateOrderLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Pre create order +func NewPreCreateOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PreCreateOrderLogic { + return &PreCreateOrderLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (resp *types.PreOrderResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + // find subscribe plan + sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribeId) + if err != nil { + l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", req.SubscribeId)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error()) + } + var discount float64 = 1 + if sub.Discount != "" { + var dis []types.SubscribeDiscount + _ = json.Unmarshal([]byte(sub.Discount), &dis) + discount = getDiscount(dis, req.Quantity) + } + price := sub.UnitPrice * req.Quantity + amount := int64(float64(price) * discount) + discountAmount := price - amount + var coupon int64 + if req.Coupon != "" { + couponInfo, err := l.svcCtx.CouponModel.FindOneByCode(l.ctx, req.Coupon) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotExist), "coupon not found") + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error()) + } + if couponInfo.Count <= couponInfo.UsedCount { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponUsed), "coupon used") + } + coupon = calculateCoupon(amount, couponInfo) + } + amount -= coupon + + var deductionAmount int64 + // Check user deduction amount + if u.GiftAmount > 0 { + if u.GiftAmount >= amount { + deductionAmount = amount + amount = 0 + } else { + deductionAmount = u.GiftAmount + amount -= u.GiftAmount + } + } + var feeAmount int64 + if req.Payment != 0 { + payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) + if err != nil { + l.Logger.Error("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error()) + } + // Calculate the handling fee + if amount > 0 { + feeAmount = calculateFee(amount, payment) + } + amount += feeAmount + } + + resp = &types.PreOrderResponse{ + Price: price, + Amount: amount, + Discount: discountAmount, + GiftAmount: deductionAmount, + Coupon: req.Coupon, + CouponDiscount: coupon, + FeeAmount: feeAmount, + } + return +} diff --git a/internal/logic/public/order/purchaseLogic.go b/internal/logic/public/order/purchaseLogic.go new file mode 100644 index 0000000..a559f38 --- /dev/null +++ b/internal/logic/public/order/purchaseLogic.go @@ -0,0 +1,217 @@ +package order + +import ( + "context" + "encoding/json" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/model/order" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + queue "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type PurchaseLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +const ( + CloseOrderTimeMinutes = 15 +) + +// NewPurchaseLogic purchase Subscription +func NewPurchaseLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PurchaseLogic { + return &PurchaseLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.PurchaseOrderResponse, err error) { + + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + // find user subscription + userSub, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id) + if err != nil { + l.Errorw("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscription error: %v", err.Error()) + } + if l.svcCtx.Config.Subscribe.SingleModel { + if len(userSub) > 0 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserSubscribeExist), "user has subscription") + } + } + + // find subscribe plan + sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribeId) + + if err != nil { + l.Errorw("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", req.SubscribeId)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error()) + } + // check subscribe plan status + if !*sub.Sell { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "subscribe not sell") + } + // check subscribe plan limit + if sub.Quota > 0 { + var count int64 + for _, v := range userSub { + if v.SubscribeId == req.SubscribeId { + count += 1 + } + } + if count >= sub.Quota { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeQuotaLimit), "quota limit") + } + } + + var discount float64 = 1 + if sub.Discount != "" { + var dis []types.SubscribeDiscount + _ = json.Unmarshal([]byte(sub.Discount), &dis) + discount = getDiscount(dis, req.Quantity) + } + price := sub.UnitPrice * req.Quantity + // discount amount + amount := int64(float64(price) * discount) + discountAmount := price - amount + var coupon int64 = 0 + // Calculate the coupon deduction + if req.Coupon != "" { + couponInfo, err := l.svcCtx.CouponModel.FindOneByCode(l.ctx, req.Coupon) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotExist), "coupon not found") + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error()) + } + if couponInfo.Count <= couponInfo.UsedCount { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponUsed), "coupon used") + } + couponSub := tool.StringToInt64Slice(couponInfo.Subscribe) + if len(couponSub) > 0 && !tool.Contains(couponSub, req.SubscribeId) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotMatch), "coupon not match") + } + coupon = calculateCoupon(amount, couponInfo) + } + // Calculate the handling fee + amount -= coupon + var deductionAmount int64 + // Check user deduction amount + if u.GiftAmount > 0 { + if u.GiftAmount >= amount { + deductionAmount = amount + amount = 0 + u.GiftAmount -= amount + } else { + deductionAmount = u.GiftAmount + amount -= u.GiftAmount + u.GiftAmount = 0 + } + } + // find payment method + payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) + if err != nil { + l.Logger.Error("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error()) + } + var feeAmount int64 + // Calculate the handling fee + if amount > 0 { + feeAmount = calculateFee(amount, payment) + } + // query user is new purchase or renewal + isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id) + if err != nil { + l.Errorw("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user order error: %v", err.Error()) + } + // create order + orderInfo := &order.Order{ + UserId: u.Id, + OrderNo: tool.GenerateTradeNo(), + Type: 1, + Quantity: req.Quantity, + Price: price, + Amount: amount, + Discount: discountAmount, + GiftAmount: deductionAmount, + Coupon: req.Coupon, + CouponDiscount: coupon, + PaymentId: payment.Id, + Method: payment.Platform, + FeeAmount: feeAmount, + Status: 1, + IsNew: isNew, + SubscribeId: req.SubscribeId, + } + // Database transaction + err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error { + // update user deduction && Pre deduction ,Return after canceling the order + if orderInfo.GiftAmount > 0 { + // update user deduction && Pre deduction ,Return after canceling the order + if e := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil { + l.Errorw("[Purchase] Database update error", logger.Field("error", err.Error()), logger.Field("user", u)) + return e + } + // create deduction record + giftAmountLog := user.GiftAmountLog{ + UserId: orderInfo.UserId, + OrderNo: orderInfo.OrderNo, + Amount: orderInfo.GiftAmount, + Type: 2, + Balance: u.GiftAmount, + Remark: "Purchase order deduction", + } + if e := db.Model(&user.GiftAmountLog{}).Create(&giftAmountLog).Error; e != nil { + l.Errorw("[Purchase] Database insert error", + logger.Field("error", err.Error()), + logger.Field("deductionLog", giftAmountLog), + ) + return e + } + } + // insert order + return db.WithContext(l.ctx).Model(&order.Order{}).Create(&orderInfo).Error + }) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert order error: %v", err.Error()) + } + // Deferred task + payload := queue.DeferCloseOrderPayload{ + OrderNo: orderInfo.OrderNo, + } + val, err := json.Marshal(payload) + if err != nil { + l.Errorw("[CreateOrder] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload)) + } + task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3)) + taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute)) + if err != nil { + l.Errorw("[CreateOrder] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task)) + } else { + l.Infow("[CreateOrder] Enqueue task success", logger.Field("TaskID", taskInfo.ID)) + } + + return &types.PurchaseOrderResponse{ + OrderNo: orderInfo.OrderNo, + }, nil +} diff --git a/internal/logic/public/order/queryOrderDetailLogic.go b/internal/logic/public/order/queryOrderDetailLogic.go new file mode 100644 index 0000000..e34392b --- /dev/null +++ b/internal/logic/public/order/queryOrderDetailLogic.go @@ -0,0 +1,40 @@ +package order + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type QueryOrderDetailLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get order +func NewQueryOrderDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryOrderDetailLogic { + return &QueryOrderDetailLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryOrderDetailLogic) QueryOrderDetail(req *types.QueryOrderDetailRequest) (resp *types.OrderDetail, err error) { + orderInfo, err := l.svcCtx.OrderModel.FindOneDetailsByOrderNo(l.ctx, req.OrderNo) + if err != nil { + l.Errorw("[QueryOrderDetail] Database query error", logger.Field("error", err.Error()), logger.Field("order_no", req.OrderNo)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find order error: %v", err.Error()) + } + resp = &types.OrderDetail{} + tool.DeepCopy(resp, orderInfo) + // Prevent commission amount leakage + resp.Commission = 0 + return +} diff --git a/internal/logic/public/order/queryOrderListLogic.go b/internal/logic/public/order/queryOrderListLogic.go new file mode 100644 index 0000000..544b157 --- /dev/null +++ b/internal/logic/public/order/queryOrderListLogic.go @@ -0,0 +1,56 @@ +package order + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type QueryOrderListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get order list +func NewQueryOrderListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryOrderListLogic { + return &QueryOrderListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryOrderListLogic) QueryOrderList(req *types.QueryOrderListRequest) (resp *types.QueryOrderListResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + total, data, err := l.svcCtx.OrderModel.QueryOrderListByPage(l.ctx, req.Page, req.Size, 0, u.Id, 0, "") + if err != nil { + l.Errorw("[QueryOrderListLogic] Query order list failed", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query order list failed") + } + resp = &types.QueryOrderListResponse{ + Total: total, + List: make([]types.OrderDetail, 0), + } + for _, item := range data { + var orderInfo types.OrderDetail + tool.DeepCopy(&orderInfo, item) + // Prevent commission amount leakage + orderInfo.Commission = 0 + resp.List = append(resp.List, orderInfo) + } + + return +} diff --git a/internal/logic/public/order/rechargeLogic.go b/internal/logic/public/order/rechargeLogic.go new file mode 100644 index 0000000..a42d3bf --- /dev/null +++ b/internal/logic/public/order/rechargeLogic.go @@ -0,0 +1,92 @@ +package order + +import ( + "context" + "encoding/json" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/model/order" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + queue "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/pkg/errors" +) + +type RechargeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Recharge +func NewRechargeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RechargeLogic { + return &RechargeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *RechargeLogic) Recharge(req *types.RechargeOrderRequest) (resp *types.RechargeOrderResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + // find payment method + payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) + if err != nil { + l.Errorw("[Recharge] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) + return nil, errors.Wrapf(err, "find payment error: %v", err.Error()) + } + // Calculate the handling fee + feeAmount := calculateFee(req.Amount, payment) + // query user is new purchase or renewal + isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id) + if err != nil { + l.Errorw("[Recharge] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id)) + return nil, errors.Wrapf(err, "query user error: %v", err.Error()) + } + orderInfo := order.Order{ + UserId: u.Id, + OrderNo: tool.GenerateTradeNo(), + Type: 4, + Price: req.Amount, + Amount: req.Amount + feeAmount, + FeeAmount: feeAmount, + PaymentId: payment.Id, + Method: payment.Platform, + Status: 1, + IsNew: isNew, + } + err = l.svcCtx.OrderModel.Insert(l.ctx, &orderInfo) + if err != nil { + l.Errorw("[Recharge] Database insert error", logger.Field("error", err.Error()), logger.Field("order", orderInfo)) + return nil, errors.Wrapf(err, "insert order error: %v", err.Error()) + } + // Deferred task + payload := queue.DeferCloseOrderPayload{ + OrderNo: orderInfo.OrderNo, + } + val, err := json.Marshal(payload) + if err != nil { + l.Errorw("[Recharge] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload)) + } + task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3)) + taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute)) + if err != nil { + l.Errorw("[Recharge] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task)) + } else { + l.Infow("[Recharge] Enqueue task success", logger.Field("TaskID", taskInfo.ID)) + } + return &types.RechargeOrderResponse{ + OrderNo: orderInfo.OrderNo, + }, nil +} diff --git a/internal/logic/public/order/renewalLogic.go b/internal/logic/public/order/renewalLogic.go new file mode 100644 index 0000000..ad59f72 --- /dev/null +++ b/internal/logic/public/order/renewalLogic.go @@ -0,0 +1,181 @@ +package order + +import ( + "context" + "encoding/json" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "gorm.io/gorm" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/model/order" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + queue "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/pkg/errors" +) + +type RenewalLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Renewal Subscription +func NewRenewalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RenewalLogic { + return &RenewalLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.RenewalOrderResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + orderNo := tool.GenerateTradeNo() + // find user subscribe + userSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeID) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscribe error: %v", err.Error()) + } + // find subscription + sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, userSubscribe.SubscribeId) + if err != nil { + l.Errorw("[Renewal] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", userSubscribe.SubscribeId)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error()) + } + // check subscribe plan status + if !*sub.Sell { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "subscribe not sell") + } + var discount float64 = 1 + if sub.Discount != "" { + var dis []types.SubscribeDiscount + _ = json.Unmarshal([]byte(sub.Discount), &dis) + discount = getDiscount(dis, req.Quantity) + } + price := sub.UnitPrice * req.Quantity + amount := int64(float64(price) * discount) + discountAmount := price - amount + var coupon int64 = 0 + if req.Coupon != "" { + couponInfo, err := l.svcCtx.CouponModel.FindOneByCode(l.ctx, req.Coupon) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotExist), "coupon not found") + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error()) + } + if couponInfo.Count <= couponInfo.UsedCount { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponUsed), "coupon used") + } + coupon = calculateCoupon(amount, couponInfo) + } + payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) + if err != nil { + l.Errorw("[Renewal] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) + return nil, errors.Wrapf(err, "find payment error: %v", err.Error()) + } + amount -= coupon + + var deductionAmount int64 + // Check user deduction amount + if u.GiftAmount > 0 { + if u.GiftAmount >= amount { + deductionAmount = amount + amount = 0 + u.GiftAmount -= amount + } else { + deductionAmount = u.GiftAmount + amount -= u.GiftAmount + u.GiftAmount = 0 + } + } + + var feeAmount int64 + // Calculate the handling fee + if amount > 0 { + feeAmount = calculateFee(amount, payment) + } + + amount += feeAmount + + // create order + orderInfo := order.Order{ + UserId: u.Id, + ParentId: userSubscribe.OrderId, + OrderNo: orderNo, + Type: 2, + Quantity: req.Quantity, + Price: price, + Amount: amount, + GiftAmount: deductionAmount, + Discount: discountAmount, + Coupon: req.Coupon, + CouponDiscount: coupon, + PaymentId: payment.Id, + Method: payment.Platform, + FeeAmount: feeAmount, + Status: 1, + SubscribeId: userSubscribe.SubscribeId, + SubscribeToken: userSubscribe.Token, + } + // Database transaction + err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error { + // update user deduction && Pre deduction ,Return after canceling the order + if orderInfo.GiftAmount > 0 { + // update user deduction && Pre deduction ,Return after canceling the order + if err := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil { + l.Errorw("[Purchase] Database update error", logger.Field("error", err.Error()), logger.Field("user", u)) + return err + } + // create deduction record + deductionLog := user.GiftAmountLog{ + UserId: orderInfo.UserId, + OrderNo: orderInfo.OrderNo, + Amount: orderInfo.GiftAmount, + Type: 2, + Balance: u.GiftAmount, + Remark: "Renewal order deduction", + } + if err := db.Model(&user.GiftAmountLog{}).Create(&deductionLog).Error; err != nil { + l.Errorw("[Renewal] Database insert error", logger.Field("error", err.Error()), logger.Field("deductionLog", deductionLog)) + return err + } + } + // insert order + return db.Model(&order.Order{}).Create(&orderInfo).Error + }) + if err != nil { + l.Errorw("[Renewal] Database insert error", logger.Field("error", err.Error()), logger.Field("order", orderInfo)) + return nil, errors.Wrapf(err, "insert order error: %v", err.Error()) + } + // Deferred task + payload := queue.DeferCloseOrderPayload{ + OrderNo: orderInfo.OrderNo, + } + val, err := json.Marshal(payload) + if err != nil { + l.Errorw("[Renewal] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload)) + } + task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3)) + taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute)) + if err != nil { + l.Errorw("[Renewal] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task)) + } else { + l.Infow("[Renewal] Enqueue task success", logger.Field("TaskID", taskInfo.ID)) + } + return &types.RenewalOrderResponse{ + OrderNo: orderInfo.OrderNo, + }, nil +} diff --git a/internal/logic/public/order/resetTrafficLogic.go b/internal/logic/public/order/resetTrafficLogic.go new file mode 100644 index 0000000..2f3ecc1 --- /dev/null +++ b/internal/logic/public/order/resetTrafficLogic.go @@ -0,0 +1,145 @@ +package order + +import ( + "context" + "encoding/json" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + + "gorm.io/gorm" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/model/order" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + queue "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/pkg/errors" +) + +type ResetTrafficLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Reset traffic +func NewResetTrafficLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetTrafficLogic { + return &ResetTrafficLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ResetTrafficLogic) ResetTraffic(req *types.ResetTrafficOrderRequest) (resp *types.ResetTrafficOrderResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + // find user subscription + userSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeID) + if err != nil { + l.Errorw("[ResetTraffic] Database query error", logger.Field("error", err.Error()), logger.Field("UserSubscribeID", req.UserSubscribeID)) + return nil, errors.Wrapf(err, "find user subscribe error: %v", err.Error()) + } + if userSubscribe.Subscribe == nil { + l.Errorw("[ResetTraffic] subscribe not found", logger.Field("UserSubscribeID", req.UserSubscribeID)) + return nil, errors.New("subscribe not found") + } + amount := userSubscribe.Subscribe.Replacement + var deductionAmount int64 + // Check user deduction amount + if u.GiftAmount > 0 { + if u.GiftAmount >= amount { + deductionAmount = amount + amount = 0 + u.GiftAmount -= amount + } else { + deductionAmount = u.GiftAmount + amount -= u.GiftAmount + u.GiftAmount = 0 + } + } + // find payment method + payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) + if err != nil { + l.Errorw("[ResetTraffic] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) + return nil, errors.Wrapf(err, "find payment error: %v", err.Error()) + } + var feeAmount int64 + // Calculate the handling fee + if amount > 0 { + feeAmount = calculateFee(amount, payment) + } + // create order + orderInfo := order.Order{ + Id: 0, + ParentId: userSubscribe.OrderId, + UserId: u.Id, + OrderNo: tool.GenerateTradeNo(), + Type: 3, + Price: userSubscribe.Subscribe.Replacement, + Amount: amount + feeAmount, + GiftAmount: deductionAmount, + FeeAmount: feeAmount, + PaymentId: payment.Id, + Method: payment.Platform, + Status: 1, + SubscribeId: userSubscribe.SubscribeId, + SubscribeToken: userSubscribe.Token, + } + // Database transaction + err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error { + // update user deduction && Pre deduction ,Return after canceling the order + if orderInfo.GiftAmount > 0 { + // update user deduction && Pre deduction ,Return after canceling the order + if err := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil { + l.Errorw("[ResetTraffic] Database update error", logger.Field("error", err.Error()), logger.Field("user", u)) + return err + } + // create deduction record + deductionLog := user.GiftAmountLog{ + UserId: orderInfo.UserId, + OrderNo: orderInfo.OrderNo, + Amount: orderInfo.GiftAmount, + Type: 2, + Balance: u.GiftAmount, + Remark: "ResetTraffic order deduction", + } + if err := db.Model(&user.GiftAmountLog{}).Create(&deductionLog).Error; err != nil { + l.Errorw("[ResetTraffic] Database insert error", logger.Field("error", err.Error()), logger.Field("deductionLog", deductionLog)) + return err + } + } + // insert order + return db.Model(&order.Order{}).Create(&orderInfo).Error + }) + if err != nil { + l.Errorw("[ResetTraffic] Database insert error", logger.Field("error", err.Error()), logger.Field("order", orderInfo)) + return nil, errors.Wrapf(err, "insert order error: %v", err.Error()) + } + // Deferred task + payload := queue.DeferCloseOrderPayload{ + OrderNo: orderInfo.OrderNo, + } + val, err := json.Marshal(payload) + if err != nil { + l.Errorw("[ResetTraffic] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload)) + } + task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3)) + taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute)) + if err != nil { + l.Errorw("[ResetTraffic] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task)) + } else { + l.Infow("[ResetTraffic] Enqueue task success", logger.Field("TaskID", taskInfo.ID)) + } + return &types.ResetTrafficOrderResponse{ + OrderNo: orderInfo.OrderNo, + }, nil +} diff --git a/internal/logic/public/payment/getAvailablePaymentMethodsLogic.go b/internal/logic/public/payment/getAvailablePaymentMethodsLogic.go new file mode 100644 index 0000000..835f90d --- /dev/null +++ b/internal/logic/public/payment/getAvailablePaymentMethodsLogic.go @@ -0,0 +1,42 @@ +package payment + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetAvailablePaymentMethodsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get available payment methods +func NewGetAvailablePaymentMethodsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAvailablePaymentMethodsLogic { + return &GetAvailablePaymentMethodsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAvailablePaymentMethodsLogic) GetAvailablePaymentMethods() (resp *types.GetAvailablePaymentMethodsResponse, err error) { + data, err := l.svcCtx.PaymentModel.FindAvailableMethods(l.ctx) + if err != nil { + l.Errorw("[GetAvailablePaymentMethods] database error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetAvailablePaymentMethods: %v", err.Error()) + } + resp = &types.GetAvailablePaymentMethodsResponse{ + List: make([]types.PaymentMethod, 0), + } + + tool.DeepCopy(&resp.List, data) + + return +} diff --git a/internal/logic/public/portal/getAvailablePaymentMethodsLogic.go b/internal/logic/public/portal/getAvailablePaymentMethodsLogic.go new file mode 100644 index 0000000..8e5faad --- /dev/null +++ b/internal/logic/public/portal/getAvailablePaymentMethodsLogic.go @@ -0,0 +1,41 @@ +package portal + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetAvailablePaymentMethodsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetAvailablePaymentMethodsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAvailablePaymentMethodsLogic { + return &GetAvailablePaymentMethodsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAvailablePaymentMethodsLogic) GetAvailablePaymentMethods() (resp *types.GetAvailablePaymentMethodsResponse, err error) { + data, err := l.svcCtx.PaymentModel.FindAvailableMethods(l.ctx) + if err != nil { + l.Errorw("[GetAvailablePaymentMethods] database error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetAvailablePaymentMethods: %v", err.Error()) + } + resp = &types.GetAvailablePaymentMethodsResponse{ + List: make([]types.PaymentMethod, 0), + } + + tool.DeepCopy(&resp.List, data) + + return +} diff --git a/internal/logic/public/portal/getSubscriptionLogic.go b/internal/logic/public/portal/getSubscriptionLogic.go new file mode 100644 index 0000000..df17f8a --- /dev/null +++ b/internal/logic/public/portal/getSubscriptionLogic.go @@ -0,0 +1,54 @@ +package portal + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetSubscriptionLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewGetSubscriptionLogic Get Subscription +func NewGetSubscriptionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSubscriptionLogic { + return &GetSubscriptionLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetSubscriptionLogic) GetSubscription() (resp *types.GetSubscriptionResponse, err error) { + resp = &types.GetSubscriptionResponse{ + List: make([]types.Subscribe, 0), + } + // Get the subscription list + data, err := l.svcCtx.SubscribeModel.QuerySubscribeListByShow(l.ctx) + if err != nil { + l.Errorw("[Site GetSubscription]", logger.Field("err", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get subscription list error: %v", err.Error()) + } + list := make([]types.Subscribe, len(data)) + for i, item := range data { + var sub types.Subscribe + tool.DeepCopy(&sub, item) + if item.Discount != "" { + var discount []types.SubscribeDiscount + _ = json.Unmarshal([]byte(item.Discount), &discount) + sub.Discount = discount + list[i] = sub + } + list[i] = sub + } + resp.List = list + return +} diff --git a/internal/logic/public/portal/prePurchaseOrderLogic.go b/internal/logic/public/portal/prePurchaseOrderLogic.go new file mode 100644 index 0000000..3d51dbe --- /dev/null +++ b/internal/logic/public/portal/prePurchaseOrderLogic.go @@ -0,0 +1,84 @@ +package portal + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type PrePurchaseOrderLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Pre Purchase Order +func NewPrePurchaseOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PrePurchaseOrderLogic { + return &PrePurchaseOrderLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *PrePurchaseOrderLogic) PrePurchaseOrder(req *types.PrePurchaseOrderRequest) (resp *types.PrePurchaseOrderResponse, err error) { + // find subscribe plan + sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribeId) + if err != nil { + l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", req.SubscribeId)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error()) + } + var discount float64 = 1 + if sub.Discount != "" { + var dis []types.SubscribeDiscount + _ = json.Unmarshal([]byte(sub.Discount), &dis) + discount = getDiscount(dis, req.Quantity) + } + price := sub.UnitPrice * req.Quantity + amount := int64(float64(price) * discount) + discountAmount := price - amount + var coupon int64 + if req.Coupon != "" { + couponInfo, err := l.svcCtx.CouponModel.FindOneByCode(l.ctx, req.Coupon) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotExist), "coupon not found") + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error()) + } + if couponInfo.Count <= couponInfo.UsedCount { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponUsed), "coupon used") + } + coupon = calculateCoupon(amount, couponInfo) + } + amount -= coupon + var feeAmount int64 + if req.Payment != 0 { + payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) + if err != nil { + l.Logger.Error("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error()) + } + // Calculate the handling fee + if amount > 0 { + feeAmount = calculateFee(amount, payment) + } + amount += feeAmount + } + + resp = &types.PrePurchaseOrderResponse{ + Price: price, + Amount: amount, + Discount: discountAmount, + Coupon: req.Coupon, + CouponDiscount: coupon, + FeeAmount: feeAmount, + } + return +} diff --git a/internal/logic/public/portal/purchaseCheckoutLogic.go b/internal/logic/public/portal/purchaseCheckoutLogic.go new file mode 100644 index 0000000..b991188 --- /dev/null +++ b/internal/logic/public/portal/purchaseCheckoutLogic.go @@ -0,0 +1,378 @@ +package portal + +import ( + "context" + "encoding/json" + "github.com/perfect-panel/ppanel-server/pkg/payment/payssion" + "strconv" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + paymentPlatform "github.com/perfect-panel/ppanel-server/pkg/payment" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/model/user" + queueType "github.com/perfect-panel/ppanel-server/queue/types" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/model/order" + "github.com/perfect-panel/ppanel-server/internal/model/payment" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/exchangeRate" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/payment/alipay" + "github.com/perfect-panel/ppanel-server/pkg/payment/epay" + "github.com/perfect-panel/ppanel-server/pkg/payment/stripe" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type PurchaseCheckoutLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewPurchaseCheckoutLogic Purchase Checkout +func NewPurchaseCheckoutLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PurchaseCheckoutLogic { + return &PurchaseCheckoutLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *PurchaseCheckoutLogic) PurchaseCheckout(req *types.CheckoutOrderRequest) (resp *types.CheckoutOrderResponse, err error) { + // Find order + orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo) + if err != nil { + l.Logger.Error("[PurchaseCheckout] Find order failed", logger.Field("error", err.Error()), logger.Field("orderNo", req.OrderNo)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.OrderNotExist), "order not exist: %v", req.OrderNo) + } + + if orderInfo.Status != 1 { + l.Logger.Error("[PurchaseCheckout] Order status error", logger.Field("status", orderInfo.Status)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.OrderStatusError), "order status error: %v", orderInfo.Status) + } + + // find payment method + paymentConfig, err := l.svcCtx.PaymentModel.FindOne(l.ctx, orderInfo.PaymentId) + if err != nil { + l.Logger.Error("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("payment", orderInfo.Method)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error()) + } + switch paymentPlatform.ParsePlatform(orderInfo.Method) { + case paymentPlatform.EPay: + url, err := l.epayPayment(paymentConfig, orderInfo, req.ReturnUrl) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "epayPayment error: %v", err.Error()) + } + resp = &types.CheckoutOrderResponse{ + CheckoutUrl: url, + Type: "url", + } + case paymentPlatform.Stripe: + stripePayment, err := l.stripePayment(paymentConfig.Config, orderInfo, "") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "stripePayment error: %v", err.Error()) + } + resp = &types.CheckoutOrderResponse{ + Type: "stripe", + Stripe: stripePayment, + } + case paymentPlatform.AlipayF2F: + url, err := l.alipayF2fPayment(paymentConfig, orderInfo) + if err != nil { + l.Errorw("[CheckoutOrderLogic] alipayF2fPayment error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "alipayF2fPayment error: %v", err.Error()) + } + resp = &types.CheckoutOrderResponse{ + Type: "qr", + CheckoutUrl: url, + } + case paymentPlatform.Payssion: + url, err := l.payssionPayment(paymentConfig, orderInfo, req.ReturnUrl) + if err != nil { + l.Errorw("[CheckoutOrderLogic] payssionPayment error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "paymentPayment error: %v", err.Error()) + } + resp = &types.CheckoutOrderResponse{ + CheckoutUrl: url, + Type: "url", + } + case paymentPlatform.Balance: + if orderInfo.UserId == 0 { + l.Errorw("[CheckoutOrderLogic] user not found", logger.Field("userId", orderInfo.UserId)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user not found") + } + // find user + userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, orderInfo.UserId) + if err != nil { + l.Errorw("[CheckoutOrderLogic] FindOne User error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOne error: %s", err.Error()) + } + + // balance + if err = l.balancePayment(userInfo, orderInfo); err != nil { + return nil, err + } + resp = &types.CheckoutOrderResponse{ + Type: "balance", + } + + default: + l.Errorw("[CheckoutOrderLogic] payment method not found", logger.Field("method", orderInfo.Method)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "payment method not found") + } + return +} + +// alipay f2f payment +func (l *PurchaseCheckoutLogic) alipayF2fPayment(pay *payment.Payment, info *order.Order) (string, error) { + f2FConfig := payment.AlipayF2FConfig{} + if err := json.Unmarshal([]byte(pay.Config), &f2FConfig); err != nil { + l.Errorw("[PurchaseCheckoutLogic] Unmarshal error", logger.Field("error", err.Error())) + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) + } + notifyUrl := "" + if pay.Domain != "" { + notifyUrl = pay.Domain + "/v1/notify/" + pay.Platform + "/" + pay.Token + } else { + host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string) + if !ok { + host = l.svcCtx.Config.Host + } + notifyUrl = "https://" + host + "/v1/notify/" + pay.Platform + "/" + pay.Token + } + client := alipay.NewClient(alipay.Config{ + AppId: f2FConfig.AppId, + PrivateKey: f2FConfig.PrivateKey, + PublicKey: f2FConfig.PublicKey, + InvoiceName: f2FConfig.InvoiceName, + NotifyURL: notifyUrl, + }) + // Calculate the amount with exchange rate + amount, err := l.queryExchangeRate("CNY", info.Amount) + if err != nil { + l.Errorw("[CheckoutOrderLogic] queryExchangeRate error", logger.Field("error", err.Error())) + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "queryExchangeRate error: %s", err.Error()) + } + convertAmount := int64(amount * 100) + // create payment + QRCode, err := client.PreCreateTrade(l.ctx, alipay.Order{ + OrderNo: info.OrderNo, + Amount: convertAmount, + }) + if err != nil { + l.Errorw("[CheckoutOrderLogic] PreCreateTrade error", logger.Field("error", err.Error())) + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "PreCreateTrade error: %s", err.Error()) + } + return QRCode, nil +} + +// Stripe Payment +func (l *PurchaseCheckoutLogic) stripePayment(config string, info *order.Order, identifier string) (*types.StripePayment, error) { + // stripe WeChat pay or stripe alipay + stripeConfig := payment.StripeConfig{} + if err := json.Unmarshal([]byte(config), &stripeConfig); err != nil { + l.Errorw("[CheckoutOrderLogic] Unmarshal error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) + } + client := stripe.NewClient(stripe.Config{ + SecretKey: stripeConfig.SecretKey, + PublicKey: stripeConfig.PublicKey, + WebhookSecret: stripeConfig.WebhookSecret, + }) + // Calculate the amount with exchange rate + amount, err := l.queryExchangeRate("CNY", info.Amount) + if err != nil { + l.Errorw("[CheckoutOrderLogic] queryExchangeRate error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "queryExchangeRate error: %s", err.Error()) + } + convertAmount := int64(amount * 100) + // create payment + result, err := client.CreatePaymentSheet(&stripe.Order{ + OrderNo: info.OrderNo, + Subscribe: strconv.FormatInt(info.SubscribeId, 10), + Amount: convertAmount, + Currency: "cny", + Payment: stripeConfig.Payment, + }, + &stripe.User{ + Email: identifier, + }) + if err != nil { + l.Errorw("[CheckoutOrderLogic] CreatePaymentSheet error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "CreatePaymentSheet error: %s", err.Error()) + } + tradeNo := result.TradeNo + stripePayment := &types.StripePayment{ + PublishableKey: stripeConfig.PublicKey, + ClientSecret: result.ClientSecret, + Method: stripeConfig.Payment, + } + // save payment + info.TradeNo = tradeNo + err = l.svcCtx.OrderModel.Update(l.ctx, info) + if err != nil { + l.Errorw("[CheckoutOrderLogic] Update error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Update error: %s", err.Error()) + } + return stripePayment, nil +} + +func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order.Order, returnUrl string) (string, error) { + epayConfig := payment.EPayConfig{} + if err := json.Unmarshal([]byte(config.Config), &epayConfig); err != nil { + l.Errorw("[CheckoutOrderLogic] Unmarshal error", logger.Field("error", err.Error())) + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) + } + client := epay.NewClient(epayConfig.Pid, epayConfig.Url, epayConfig.Key) + // Calculate the amount with exchange rate + amount, err := l.queryExchangeRate("CNY", info.Amount) + if err != nil { + return "", err + } + notifyUrl := "" + if config.Domain != "" { + notifyUrl = config.Domain + "/v1/notify/" + config.Platform + "/" + config.Token + } else { + host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string) + if !ok { + host = l.svcCtx.Config.Host + } + notifyUrl = "https://" + host + "/v1/notify/" + config.Platform + "/" + config.Token + } + // create payment + url := client.CreatePayUrl(epay.Order{ + Name: l.svcCtx.Config.Site.SiteName, + Amount: amount, + OrderNo: info.OrderNo, + SignType: "MD5", + NotifyUrl: notifyUrl, + ReturnUrl: returnUrl, + }) + return url, nil +} + +func (l *PurchaseCheckoutLogic) payssionPayment(config *payment.Payment, info *order.Order, returnUrl string) (string, error) { + payssionConfig := payment.PayssionConfig{} + if err := json.Unmarshal([]byte(config.Config), &payssionConfig); err != nil { + l.Errorw("[CheckoutOrderLogic] payssionPayment Unmarshal error", logger.Field("error", err.Error())) + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), " payssionPaymentUnmarshal error: %s", err.Error()) + } + client := payssion.NewClient(payssionConfig.ApiKey, payssionConfig.SecretKey, payssionConfig.PmId, payssionConfig.Currency, payssionConfig.QueryUrl, payssionConfig.CreateUrl) + // Calculate the amount with exchange rate + amount, err := l.queryExchangeRate("CNY", info.Amount) + if err != nil { + l.Errorw("[CheckoutOrderLogic] payssionPayment queryExchangeRate error", logger.Field("error", err.Error())) + return "", err + } + notifyUrl := "" + if config.Domain != "" { + notifyUrl = config.Domain + "/v1/notify/" + config.Platform + "/" + config.Token + } else { + host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string) + if !ok { + host = l.svcCtx.Config.Host + } + notifyUrl = "https://" + host + "/v1/notify/" + config.Platform + "/" + config.Token + } + // create payment + url, err := client.CreateOrder(payssion.Order{ + Name: l.svcCtx.Config.Site.SiteName, + Amount: amount, + OrderNo: info.OrderNo, + NotifyUrl: notifyUrl, + ReturnUrl: returnUrl, + }) + return url, err +} + +// Query exchange rate +func (l *PurchaseCheckoutLogic) queryExchangeRate(to string, src int64) (amount float64, err error) { + amount = float64(src) / float64(100) + // query system currency + currency, err := l.svcCtx.SystemModel.GetCurrencyConfig(l.ctx) + if err != nil { + l.Errorw("[CheckoutOrderLogic] GetCurrencyConfig error", logger.Field("error", err.Error())) + return 0, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetCurrencyConfig error: %s", err.Error()) + } + configs := struct { + CurrencyUnit string + CurrencySymbol string + AccessKey string + }{} + tool.SystemConfigSliceReflectToStruct(currency, &configs) + if configs.AccessKey == "" { + return amount, nil + } + if configs.CurrencyUnit != to { + // query exchange rate + result, err := exchangeRate.GetExchangeRete(configs.CurrencyUnit, to, configs.AccessKey, 1) + if err != nil { + return 0, err + } + amount = result * amount + } + return amount, nil +} + +// Balance payment +func (l *PurchaseCheckoutLogic) balancePayment(u *user.User, o *order.Order) error { + var userInfo user.User + err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + err := db.Model(&user.User{}).Where("id = ?", u.Id).First(&userInfo).Error + if err != nil { + return err + } + + if userInfo.Balance < o.Amount { + return errors.Wrapf(xerr.NewErrCode(xerr.InsufficientBalance), "Insufficient balance") + } + // deduct balance + userInfo.Balance -= o.Amount + err = l.svcCtx.UserModel.Update(l.ctx, &userInfo) + if err != nil { + return err + } + // create balance log + balanceLog := &user.BalanceLog{ + Id: 0, + UserId: u.Id, + Amount: o.Amount, + Type: 3, + OrderId: o.Id, + Balance: userInfo.Balance, + } + err = db.Create(balanceLog).Error + if err != nil { + return err + } + return l.svcCtx.OrderModel.UpdateOrderStatus(l.ctx, o.OrderNo, 2) + }) + if err != nil { + l.Errorw("[CheckoutOrderLogic] Transaction error", logger.Field("error", err.Error()), logger.Field("orderNo", o.OrderNo)) + return err + } + // create activity order task + payload := queueType.ForthwithActivateOrderPayload{ + OrderNo: o.OrderNo, + } + bytes, err := json.Marshal(payload) + if err != nil { + l.Errorw("[CheckoutOrderLogic] Marshal error", logger.Field("error", err.Error())) + return err + } + + task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes) + _, err = l.svcCtx.Queue.EnqueueContext(l.ctx, task) + if err != nil { + l.Errorw("[CheckoutOrderLogic] Enqueue error", logger.Field("error", err.Error())) + return err + } + l.Logger.Info("[CheckoutOrderLogic] Enqueue success", logger.Field("orderNo", o.OrderNo)) + return nil +} diff --git a/internal/logic/public/portal/purchaseLogic.go b/internal/logic/public/portal/purchaseLogic.go new file mode 100644 index 0000000..2f445a2 --- /dev/null +++ b/internal/logic/public/portal/purchaseLogic.go @@ -0,0 +1,171 @@ +package portal + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/payment" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/model/order" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + queue "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type PurchaseLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewPurchaseLogic Purchase subscription +func NewPurchaseLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PurchaseLogic { + return &PurchaseLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +const ( + CloseOrderTimeMinutes = 15 +) + +func (l *PurchaseLogic) Purchase(req *types.PortalPurchaseRequest) (resp *types.PortalPurchaseResponse, err error) { + // find user auth + userAuth, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, req.AuthType, req.Identifier) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user auth error: %v", err.Error()) + } + if userAuth.UserId != 0 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "user already exists") + } + // find subscribe plan + sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribeId) + if err != nil { + l.Errorw("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", req.SubscribeId)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error()) + } + // check subscribe plan status + if !*sub.Sell { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "subscribe not sell") + } + var discount float64 = 1 + if sub.Discount != "" { + var dis []types.SubscribeDiscount + _ = json.Unmarshal([]byte(sub.Discount), &dis) + discount = getDiscount(dis, req.Quantity) + } + price := sub.UnitPrice * req.Quantity + // discount amount + amount := int64(float64(price) * discount) + discountAmount := price - amount + + var coupon int64 = 0 + // Calculate the coupon deduction + if req.Coupon != "" { + couponInfo, err := l.svcCtx.CouponModel.FindOneByCode(l.ctx, req.Coupon) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotExist), "coupon not found") + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error()) + } + if couponInfo.Count <= couponInfo.UsedCount { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponUsed), "coupon used") + } + couponSub := tool.StringToInt64Slice(couponInfo.Subscribe) + if len(couponSub) > 0 && !tool.Contains(couponSub, req.SubscribeId) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotMatch), "coupon not match") + } + coupon = calculateCoupon(amount, couponInfo) + } + // Calculate the handling fee + amount -= coupon + var deductionAmount int64 + // find payment method + paymentConfig, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) + if err != nil { + l.Logger.Error("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.PaymentMethodNotFound), "find payment method error: %v", err.Error()) + } + + if payment.ParsePlatform(paymentConfig.Platform) == payment.Balance { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.PaymentMethodNotFound), "balance error") + } + + var feeAmount int64 + // Calculate the handling fee + if amount > 0 { + feeAmount = calculateFee(amount, paymentConfig) + } + // create order + orderInfo := &order.Order{ + OrderNo: tool.GenerateTradeNo(), + Type: 1, + Quantity: req.Quantity, + Price: price, + Amount: amount, + Discount: discountAmount, + GiftAmount: deductionAmount, + Coupon: req.Coupon, + CouponDiscount: coupon, + PaymentId: req.Payment, + Method: paymentConfig.Platform, + FeeAmount: feeAmount, + Status: 1, + IsNew: true, + SubscribeId: req.SubscribeId, + } + // save order + err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { + // save guest order and user information + tempOrder := constant.TemporaryOrderInfo{ + OrderNo: orderInfo.OrderNo, + Identifier: req.Identifier, + AuthType: req.AuthType, + Password: req.Password, + } + if _, err = l.svcCtx.Redis.Set(l.ctx, fmt.Sprintf(constant.TempOrderCacheKey, orderInfo.OrderNo), tempOrder.Marshal(), CloseOrderTimeMinutes*time.Minute).Result(); err != nil { + l.Errorw("[Purchase] Redis set error", logger.Field("error", err.Error()), logger.Field("order_no", orderInfo.OrderNo)) + return err + } + l.Infow("[Purchase] Guest order", logger.Field("order_no", orderInfo.OrderNo), logger.Field("identifier", req.Identifier)) + // save guest order + if err := l.svcCtx.OrderModel.Insert(l.ctx, orderInfo, tx); err != nil { + return err + } + return nil + }) + if err != nil { + l.Errorw("[Purchase] Database transaction error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "transaction error: %v", err.Error()) + } + // Deferred task + payload := queue.DeferCloseOrderPayload{ + OrderNo: orderInfo.OrderNo, + } + val, err := json.Marshal(payload) + if err != nil { + l.Errorw("[CloseOrder Task] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload)) + } + task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3)) + taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute)) + if err != nil { + l.Errorw("[CloseOrder Task] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", taskInfo)) + } else { + l.Infow("[CloseOrder Task] Enqueue task success", logger.Field("TaskID", taskInfo.ID)) + } + resp = &types.PortalPurchaseResponse{OrderNo: orderInfo.OrderNo} + return resp, nil +} diff --git a/internal/logic/public/portal/queryPurchaseOrderLogic.go b/internal/logic/public/portal/queryPurchaseOrderLogic.go new file mode 100644 index 0000000..fc67dc4 --- /dev/null +++ b/internal/logic/public/portal/queryPurchaseOrderLogic.go @@ -0,0 +1,166 @@ +package portal + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/perfect-panel/ppanel-server/internal/model/order" + + "github.com/perfect-panel/ppanel-server/pkg/tool" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/jwt" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/uuidx" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type QueryPurchaseOrderLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewQueryPurchaseOrderLogic Query Purchase Order +func NewQueryPurchaseOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryPurchaseOrderLogic { + return &QueryPurchaseOrderLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// Centralized error handler for database issues +func wrapDatabaseError(err error) error { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Database Query Error: %v", err.Error()) +} + +func (l *QueryPurchaseOrderLogic) QueryPurchaseOrder(req *types.QueryPurchaseOrderRequest) (resp *types.QueryPurchaseOrderResponse, err error) { + orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo) + if err != nil { + return nil, wrapDatabaseError(err) + } + // Handle temporary orders if applicable + var token string + if orderInfo.Status == 2 || orderInfo.Status == 5 { + if token, err = l.handleTemporaryOrder(orderInfo, req); err != nil { + return nil, err + } + } + // Fetch subscription and payment information + subscribeInfo, paymentInfo, err := l.fetchOrderDetails(orderInfo) + if err != nil { + return nil, err + } + + return &types.QueryPurchaseOrderResponse{ + OrderNo: orderInfo.OrderNo, + Subscribe: subscribeInfo, + Quantity: orderInfo.Quantity, + Price: orderInfo.Price, + Amount: orderInfo.Amount, + Discount: orderInfo.Discount, + Coupon: orderInfo.Coupon, + CouponDiscount: orderInfo.CouponDiscount, + FeeAmount: orderInfo.FeeAmount, + Payment: paymentInfo, + Status: orderInfo.Status, + CreatedAt: orderInfo.CreatedAt.UnixMilli(), + Token: token, + }, nil +} + +// handleTemporaryOrder processes temporary order-related operations +func (l *QueryPurchaseOrderLogic) handleTemporaryOrder(orderInfo *order.Order, req *types.QueryPurchaseOrderRequest) (string, error) { + cacheKey := fmt.Sprintf(constant.TempOrderCacheKey, orderInfo.OrderNo) + cacheValue, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() + if err != nil { + l.Errorw("Get TempOrderCacheKey Error", logger.Field("cacheKey", cacheKey), logger.Field("error", err.Error())) + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Get TempOrderCacheKey Error: %v", err.Error()) + } + + var tempOrder constant.TemporaryOrderInfo + if err := json.Unmarshal([]byte(cacheValue), &tempOrder); err != nil { + l.Errorw("JSON Unmarshal Error", logger.Field("error", err.Error()), logger.Field("cacheValue", cacheValue)) + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "JSON Unmarshal Error: %v", err.Error()) + } + if tempOrder.OrderNo != orderInfo.OrderNo { + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Order number mismatch") + } + + // Validate user and email + if err := l.validateUserAndEmail(orderInfo, req.Identifier, req.Identifier); err != nil { + return "", err + } + + // Generate session token + return l.generateSessionToken(orderInfo.UserId) +} + +// validateUserAndEmail ensures the user and email are correct +func (l *QueryPurchaseOrderLogic) validateUserAndEmail(orderInfo *order.Order, platform, openid string) error { + userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, orderInfo.UserId) + if err != nil { + return wrapDatabaseError(err) + } + + authMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, platform, openid) + if err != nil { + return wrapDatabaseError(err) + } + if authMethod.UserId != userInfo.Id { + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Email verification failed") + } + + return nil +} + +// generateSessionToken creates a session token and stores it in Redis +func (l *QueryPurchaseOrderLogic) generateSessionToken(userId int64) (string, error) { + sessionId := uuidx.NewUUID().String() + token, err := jwt.NewJwtToken( + l.svcCtx.Config.JwtAuth.AccessSecret, + time.Now().Unix(), + l.svcCtx.Config.JwtAuth.AccessExpire, + jwt.WithOption("UserId", userId), + jwt.WithOption("SessionId", sessionId), + ) + if err != nil { + l.Errorw("Token Generation Error", logger.Field("error", err.Error())) + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Token generation error") + } + + cacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) + if err := l.svcCtx.Redis.Set(l.ctx, cacheKey, userId, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Session storage error") + } + + return token, nil +} + +// fetchOrderDetails retrieves subscription and payment details +func (l *QueryPurchaseOrderLogic) fetchOrderDetails(orderInfo *order.Order) (types.Subscribe, types.PaymentMethod, error) { + sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, orderInfo.SubscribeId) + if err != nil { + return types.Subscribe{}, types.PaymentMethod{}, wrapDatabaseError(err) + } + + var subscribeInfo types.Subscribe + tool.DeepCopy(&subscribeInfo, sub) + + payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, orderInfo.PaymentId) + if err != nil { + return types.Subscribe{}, types.PaymentMethod{}, wrapDatabaseError(err) + } + + var paymentInfo types.PaymentMethod + tool.DeepCopy(&paymentInfo, payment) + + return subscribeInfo, paymentInfo, nil +} diff --git a/internal/logic/public/portal/tool.go b/internal/logic/public/portal/tool.go new file mode 100644 index 0000000..521632d --- /dev/null +++ b/internal/logic/public/portal/tool.go @@ -0,0 +1,43 @@ +package portal + +import ( + "github.com/perfect-panel/ppanel-server/internal/model/coupon" + "github.com/perfect-panel/ppanel-server/internal/model/payment" + "github.com/perfect-panel/ppanel-server/internal/types" +) + +func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 { + var finalDiscount int64 = 100 + + for _, discount := range discounts { + if inputMonths >= discount.Quantity && discount.Discount < finalDiscount { + finalDiscount = discount.Discount + } + } + return float64(finalDiscount) / float64(100) +} + +func calculateCoupon(amount int64, couponInfo *coupon.Coupon) int64 { + if couponInfo.Type == 1 { + return int64(float64(amount) * (float64(couponInfo.Discount) / float64(100))) + } else { + return min(couponInfo.Discount, amount) + } +} + +func calculateFee(amount int64, config *payment.Payment) int64 { + var fee float64 + switch config.FeeMode { + case 0: + return 0 + case 1: + fee = float64(amount) * (float64(config.FeePercent) / float64(100)) + case 2: + if amount > 0 { + fee = float64(config.FeeAmount) + } + case 3: + fee = float64(amount)*(float64(config.FeePercent)/float64(100)) + float64(config.FeeAmount) + } + return int64(fee) +} diff --git a/internal/logic/public/subscribe/queryApplicationConfigLogic.go b/internal/logic/public/subscribe/queryApplicationConfigLogic.go new file mode 100644 index 0000000..b5a913d --- /dev/null +++ b/internal/logic/public/subscribe/queryApplicationConfigLogic.go @@ -0,0 +1,116 @@ +package subscribe + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/application" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type QueryApplicationConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get application config +func NewQueryApplicationConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryApplicationConfigLogic { + return &QueryApplicationConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryApplicationConfigLogic) QueryApplicationConfig() (resp *types.ApplicationResponse, err error) { + resp = &types.ApplicationResponse{} + var applications []*application.Application + err = l.svcCtx.ApplicationModel.Transaction(l.ctx, func(tx *gorm.DB) (err error) { + return tx.Model(applications).Preload("ApplicationVersions").Find(&applications).Error + }) + if err != nil { + l.Errorw("[QueryApplicationConfig] get application error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get application error: %v", err.Error()) + } + + if len(applications) == 0 { + return resp, nil + } + + for _, app := range applications { + applicationResponse := types.ApplicationResponseInfo{ + Id: app.Id, + Name: app.Name, + Icon: app.Icon, + Description: app.Description, + SubscribeType: app.SubscribeType, + } + applicationVersions := app.ApplicationVersions + if len(applicationVersions) != 0 { + for _, applicationVersion := range applicationVersions { + /*if !applicationVersion.IsDefault { + continue + }*/ + switch applicationVersion.Platform { + case "ios": + applicationResponse.Platform.IOS = append(applicationResponse.Platform.IOS, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + case "macos": + applicationResponse.Platform.MacOS = append(applicationResponse.Platform.MacOS, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + case "linux": + applicationResponse.Platform.Linux = append(applicationResponse.Platform.Linux, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + case "android": + applicationResponse.Platform.Android = append(applicationResponse.Platform.Android, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + case "windows": + applicationResponse.Platform.Windows = append(applicationResponse.Platform.Windows, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + case "harmony": + applicationResponse.Platform.Harmony = append(applicationResponse.Platform.Harmony, &types.ApplicationVersion{ + Id: applicationVersion.Id, + Url: applicationVersion.Url, + Version: applicationVersion.Version, + IsDefault: applicationVersion.IsDefault, + Description: applicationVersion.Description, + }) + } + } + } + resp.Applications = append(resp.Applications, applicationResponse) + } + + return +} diff --git a/internal/logic/public/subscribe/querySubscribeGroupListLogic.go b/internal/logic/public/subscribe/querySubscribeGroupListLogic.go new file mode 100644 index 0000000..d0f08ac --- /dev/null +++ b/internal/logic/public/subscribe/querySubscribeGroupListLogic.go @@ -0,0 +1,44 @@ +package subscribe + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/model/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type QuerySubscribeGroupListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get subscribe group list +func NewQuerySubscribeGroupListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QuerySubscribeGroupListLogic { + return &QuerySubscribeGroupListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QuerySubscribeGroupListLogic) QuerySubscribeGroupList() (resp *types.QuerySubscribeGroupListResponse, err error) { + var list []*subscribe.Group + var total int64 + err = l.svcCtx.DB.Model(&subscribe.Group{}).Count(&total).Find(&list).Error + if err != nil { + l.Logger.Error("[QuerySubscribeGroupListLogic] get subscribe group list failed: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get subscribe group list failed: %v", err.Error()) + } + groupList := make([]types.SubscribeGroup, 0) + tool.DeepCopy(&groupList, list) + return &types.QuerySubscribeGroupListResponse{ + Total: total, + List: groupList, + }, nil +} diff --git a/internal/logic/public/subscribe/querySubscribeListLogic.go b/internal/logic/public/subscribe/querySubscribeListLogic.go new file mode 100644 index 0000000..c51b5a9 --- /dev/null +++ b/internal/logic/public/subscribe/querySubscribeListLogic.go @@ -0,0 +1,54 @@ +package subscribe + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type QuerySubscribeListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get subscribe list +func NewQuerySubscribeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QuerySubscribeListLogic { + return &QuerySubscribeListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QuerySubscribeListLogic) QuerySubscribeList() (resp *types.QuerySubscribeListResponse, err error) { + + data, err := l.svcCtx.SubscribeModel.QuerySubscribeList(l.ctx) + if err != nil { + l.Errorw("[QuerySubscribeListLogic] Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QuerySubscribeList error: %v", err.Error()) + } + resp = &types.QuerySubscribeListResponse{ + Total: int64(len(data)), + } + list := make([]types.Subscribe, len(data)) + for i, item := range data { + var sub types.Subscribe + tool.DeepCopy(&sub, item) + if item.Discount != "" { + var discount []types.SubscribeDiscount + _ = json.Unmarshal([]byte(item.Discount), &discount) + sub.Discount = discount + list[i] = sub + } + list[i] = sub + } + resp.List = list + return +} diff --git a/internal/logic/public/ticket/constant.go b/internal/logic/public/ticket/constant.go new file mode 100644 index 0000000..2656fe7 --- /dev/null +++ b/internal/logic/public/ticket/constant.go @@ -0,0 +1 @@ +package ticket diff --git a/internal/logic/public/ticket/createUserTicketFollowLogic.go b/internal/logic/public/ticket/createUserTicketFollowLogic.go new file mode 100644 index 0000000..c540931 --- /dev/null +++ b/internal/logic/public/ticket/createUserTicketFollowLogic.go @@ -0,0 +1,66 @@ +package ticket + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/ticket" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type CreateUserTicketFollowLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Create ticket follow +func NewCreateUserTicketFollowLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateUserTicketFollowLogic { + return &CreateUserTicketFollowLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateUserTicketFollowLogic) CreateUserTicketFollow(req *types.CreateUserTicketFollowRequest) error { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + // query ticket + t, err := l.svcCtx.TicketModel.FindOne(l.ctx, req.TicketId) + if err != nil { + l.Errorw("[CreateUserTicketFollow] Database query error", logger.Field("error", err.Error()), logger.Field("request", req)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query ticket failed: %v", err.Error()) + } + // check access + if u.Id != t.UserId { + l.Errorw("[CreateUserTicketFollow] Invalid access", logger.Field("user_id", u.Id), logger.Field("ticket_user_id", t.UserId)) + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "invalid access") + } + // insert follow + err = l.svcCtx.TicketModel.InsertTicketFollow(l.ctx, &ticket.Follow{ + TicketId: req.TicketId, + From: req.From, + Type: req.Type, + Content: req.Content, + }) + if err != nil { + l.Errorw("[CreateUserTicketFollow] Database insert error", logger.Field("error", err.Error()), logger.Field("request", req)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create ticket follow failed: %v", err.Error()) + } + err = l.svcCtx.TicketModel.UpdateTicketStatus(l.ctx, req.TicketId, u.Id, ticket.Pending) + if err != nil { + l.Errorw("[CreateUserTicketFollow] Database update error", logger.Field("error", err.Error()), logger.Field("status", ticket.Pending)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update ticket status failed: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/public/ticket/createUserTicketLogic.go b/internal/logic/public/ticket/createUserTicketLogic.go new file mode 100644 index 0000000..51ce680 --- /dev/null +++ b/internal/logic/public/ticket/createUserTicketLogic.go @@ -0,0 +1,49 @@ +package ticket + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/ticket" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type CreateUserTicketLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Create ticket +func NewCreateUserTicketLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateUserTicketLogic { + return &CreateUserTicketLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateUserTicketLogic) CreateUserTicket(req *types.CreateUserTicketRequest) error { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + err := l.svcCtx.TicketModel.Insert(l.ctx, &ticket.Ticket{ + Title: req.Title, + Description: req.Description, + UserId: u.Id, + Status: ticket.Pending, + }) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert ticket error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/public/ticket/getUserTicketDetailsLogic.go b/internal/logic/public/ticket/getUserTicketDetailsLogic.go new file mode 100644 index 0000000..b22b3d5 --- /dev/null +++ b/internal/logic/public/ticket/getUserTicketDetailsLogic.go @@ -0,0 +1,51 @@ +package ticket + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetUserTicketDetailsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get ticket detail +func NewGetUserTicketDetailsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserTicketDetailsLogic { + return &GetUserTicketDetailsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetUserTicketDetailsLogic) GetUserTicketDetails(req *types.GetUserTicketDetailRequest) (resp *types.Ticket, err error) { + + data, err := l.svcCtx.TicketModel.QueryTicketDetail(l.ctx, req.Id) + if err != nil { + l.Errorw("[GetUserTicketDetailsLogic] Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get ticket detail failed: %v", err.Error()) + } + // check access + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + if data.UserId != u.Id { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "invalid access") + } + resp = &types.Ticket{} + tool.DeepCopy(resp, data) + return +} diff --git a/internal/logic/public/ticket/getUserTicketListLogic.go b/internal/logic/public/ticket/getUserTicketListLogic.go new file mode 100644 index 0000000..fdaa92c --- /dev/null +++ b/internal/logic/public/ticket/getUserTicketListLogic.go @@ -0,0 +1,50 @@ +package ticket + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetUserTicketListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get ticket list +func NewGetUserTicketListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserTicketListLogic { + return &GetUserTicketListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetUserTicketListLogic) GetUserTicketList(req *types.GetUserTicketListRequest) (resp *types.GetUserTicketListResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + l.Logger.Debugf("Current user: %v", u.Id) + total, list, err := l.svcCtx.TicketModel.QueryTicketList(l.ctx, req.Page, req.Size, u.Id, req.Status, req.Search) + if err != nil { + l.Errorw("[GetUserTicketListLogic] Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryTicketList error: %v", err) + } + resp = &types.GetUserTicketListResponse{ + Total: total, + List: make([]types.Ticket, 0), + } + tool.DeepCopy(&resp.List, list) + return +} diff --git a/internal/logic/public/ticket/updateUserTicketStatusLogic.go b/internal/logic/public/ticket/updateUserTicketStatusLogic.go new file mode 100644 index 0000000..acf71f7 --- /dev/null +++ b/internal/logic/public/ticket/updateUserTicketStatusLogic.go @@ -0,0 +1,43 @@ +package ticket + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateUserTicketStatusLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Update ticket status +func NewUpdateUserTicketStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserTicketStatusLogic { + return &UpdateUserTicketStatusLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateUserTicketStatusLogic) UpdateUserTicketStatus(req *types.UpdateUserTicketStatusRequest) error { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + err := l.svcCtx.TicketModel.UpdateTicketStatus(l.ctx, req.Id, u.Id, *req.Status) + if err != nil { + l.Errorw("[UpdateUserTicketStatusLogic] Database Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update ticket error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/public/user/bindOAuthCallbackLogic.go b/internal/logic/public/user/bindOAuthCallbackLogic.go new file mode 100644 index 0000000..dc4044f --- /dev/null +++ b/internal/logic/public/user/bindOAuthCallbackLogic.go @@ -0,0 +1,214 @@ +package user + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/auth" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/oauth/apple" + "github.com/perfect-panel/ppanel-server/pkg/oauth/google" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type BindOAuthCallbackLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Bind OAuth Callback +func NewBindOAuthCallbackLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindOAuthCallbackLogic { + return &BindOAuthCallbackLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +type googleRequest struct { + Code string `json:"code"` + State string `json:"state"` +} + +func (l *BindOAuthCallbackLogic) BindOAuthCallback(req *types.BindOAuthCallbackRequest) error { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + var err error + switch req.Method { + case "google": + err = l.google(req) + case "apple": + err = l.apple(req) + default: + l.Errorw("oauth login method not support: %v", logger.Field("method", req.Method)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "oauth login method not support: %v", req.Method) + } + if err != nil { + l.Errorw("bind oauth callback failed: %v", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "bind oauth callback failed") + } + // update user info to redis + err = l.svcCtx.UserModel.UpdateUserCache(l.ctx, u) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "update user cache failed") + } + + return nil +} +func (l *BindOAuthCallbackLogic) google(req *types.BindOAuthCallbackRequest) error { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + var request googleRequest + err := tool.CloneMapToStruct(req.Callback.(map[string]interface{}), &request) + if err != nil { + l.Errorw("error CloneMapToStruct: %v", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "CloneMapToStruct failed") + } + // validate the state code + redirect, err := l.svcCtx.Redis.Get(l.ctx, fmt.Sprintf("google:%s", request.State)).Result() + if err != nil { + l.Errorw("error get google state code: %v", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "get google state code failed") + } + // get google config + authMethod, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, "google") + if err != nil { + l.Errorw("error find google auth method: %v", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find google auth method failed") + } + var cfg auth.GoogleAuthConfig + err = json.Unmarshal([]byte(authMethod.Config), &cfg) + if err != nil { + l.Errorw("error unmarshal google config: %v", logger.Field("config", authMethod.Config), logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "unmarshal google config failed") + } + client := google.New(&google.Config{ + ClientID: cfg.ClientId, + ClientSecret: cfg.ClientSecret, + RedirectURL: redirect, + }) + token, err := client.Exchange(l.ctx, request.Code) + if err != nil { + l.Errorw("error exchange google token: %v", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "exchange google token failed") + } + googleUserInfo, err := client.GetUserInfo(token.AccessToken) + if err != nil { + l.Errorw("error get google user info: %v", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "get google user info failed") + } + // query user info + userAuthMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "google", googleUserInfo.OpenID) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user auth method failed") + } + if userAuthMethod.Id > 0 { + return errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "google user already exists") + } + // bind google + userAuthMethod = &user.AuthMethods{ + UserId: u.Id, + AuthType: "google", + AuthIdentifier: googleUserInfo.OpenID, + Verified: true, + } + err = l.svcCtx.UserModel.InsertUserAuthMethods(l.ctx, userAuthMethod) + if err != nil { + l.Errorw("error insert user auth method: %v", logger.Field("error", err.Error())) + return err + } + return nil +} + +func (l *BindOAuthCallbackLogic) apple(req *types.BindOAuthCallbackRequest) error { + // validate the state code + _, err := l.svcCtx.Redis.Get(l.ctx, fmt.Sprintf("apple:%s", req.Callback.(map[string]interface{})["state"])).Result() + if err != nil { + l.Errorw("[BindOAuthCallbackLogic] Get State code error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "get apple state code failed: %v", err.Error()) + } + appleAuth, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, "apple") + if err != nil { + l.Errorw("[BindOAuthCallbackLogic] FindOneByMethod error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find apple auth method failed: %v", err.Error()) + } + var appleCfg auth.AppleAuthConfig + err = json.Unmarshal([]byte(appleAuth.Config), &appleCfg) + if err != nil { + l.Errorw("[BindOAuthCallbackLogic] Unmarshal error", logger.Field("error", err.Error()), logger.Field("config", appleAuth.Config)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "unmarshal apple config failed: %v", err.Error()) + } + + client, err := apple.New(apple.Config{ + ClientID: appleCfg.ClientId, + TeamID: appleCfg.TeamID, + KeyID: appleCfg.KeyID, + ClientSecret: appleCfg.ClientSecret, + RedirectURI: appleCfg.RedirectURL, + }) + if err != nil { + l.Errorw("[BindOAuthCallbackLogic] New apple client error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "new apple client failed: %v", err.Error()) + } + // verify web token + resp, err := client.VerifyWebToken(l.ctx, req.Callback.(map[string]interface{})["code"].(string)) + if err != nil { + l.Errorw("[BindOAuthCallbackLogic] VerifyWebToken error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "verify web token failed: %v", err.Error()) + } + if resp.Error != "" { + l.Errorw("[BindOAuthCallbackLogic] VerifyWebToken error", logger.Field("error", resp.Error)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "verify web token failed: %v", resp.Error) + } + // query apple user unique id + appleUnique, err := apple.GetUniqueID(resp.IDToken) + if err != nil { + l.Errorw("[BindOAuthCallbackLogic] GetUniqueID error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "get apple unique id failed: %v", err.Error()) + } + // query user by apple unique id + userAuthMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "apple", appleUnique) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorw("[BindOAuthCallbackLogic] FindUserAuthMethodByOpenID error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user auth method by openid failed: %v", err.Error()) + } + if userAuthMethod.Id > 0 { + l.Errorw("[BindOAuthCallbackLogic] User already exists") + return errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "apple user already exists") + } + // query user info + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + // bind apple + userAuthMethod = &user.AuthMethods{ + UserId: u.Id, + AuthType: "apple", + AuthIdentifier: appleUnique, + Verified: true, + } + err = l.svcCtx.UserModel.InsertUserAuthMethods(l.ctx, userAuthMethod) + if err != nil { + l.Errorw("[BindOAuthCallbackLogic] InsertUserAuthMethods error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert user auth method failed: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/public/user/bindOAuthLogic.go b/internal/logic/public/user/bindOAuthLogic.go new file mode 100644 index 0000000..b6acfe1 --- /dev/null +++ b/internal/logic/public/user/bindOAuthLogic.go @@ -0,0 +1,112 @@ +package user + +import ( + "context" + "fmt" + "time" + + "github.com/perfect-panel/ppanel-server/internal/model/auth" + "github.com/perfect-panel/ppanel-server/pkg/oauth/google" + "github.com/perfect-panel/ppanel-server/pkg/random" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "golang.org/x/oauth2" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type BindOAuthLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Bind OAuth +func NewBindOAuthLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindOAuthLogic { + return &BindOAuthLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *BindOAuthLogic) BindOAuth(req *types.BindOAuthRequest) (resp *types.BindOAuthResponse, err error) { + var uri string + switch req.Method { + case "google": + uri, err = l.google(req) + case "apple": + uri, err = l.apple(req) + case "github": + uri, err = l.github() + case "facebook": + uri, err = l.facebook() + default: + l.Errorw("oauth login method not support: %v", logger.Field("method", req.Method)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "oauth login method not support: %v", req.Method) + } + if err != nil { + l.Errorw("error bind oauth", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "error bind oauth: %v", err.Error()) + } + return &types.BindOAuthResponse{ + Redirect: uri, + }, nil +} + +func (l *BindOAuthLogic) google(req *types.BindOAuthRequest) (string, error) { + authMethod, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, "google") + if err != nil { + return "", err + } + cfg := new(auth.GoogleAuthConfig) + err = cfg.Unmarshal(authMethod.Config) + if err != nil { + l.Errorw("error unmarshal google config: %v", logger.Field("config", authMethod.Config), logger.Field("error", err.Error())) + return "", err + } + client := google.New(&google.Config{ + ClientID: cfg.ClientId, + ClientSecret: cfg.ClientSecret, + RedirectURL: req.Redirect, + }) + // generate the state code + code := random.KeyNew(8, 1) + // save the state code + err = l.svcCtx.Redis.Set(l.ctx, fmt.Sprintf("google:%s", code), req.Redirect, 5*60*time.Second).Err() + if err != nil { + return "", err + } + uri := client.AuthCodeURL(code, oauth2.AccessTypeOffline) + return uri, nil +} + +func (l *BindOAuthLogic) facebook() (string, error) { + return "", nil +} +func (l *BindOAuthLogic) apple(req *types.BindOAuthRequest) (string, error) { + authMethod, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, "apple") + if err != nil { + return "", err + } + var cfg auth.AppleAuthConfig + err = cfg.Unmarshal(authMethod.Config) + if err != nil { + l.Errorw("error unmarshal apple config: %v", logger.Field("config", authMethod.Config), logger.Field("error", err.Error())) + return "", err + } + uri := "https://appleid.apple.com/auth/authorize?client_id=%s&redirect_uri=%s&response_type=code&state=%s&scope=name email&response_mode=form_post" + // generate the state code + code := random.KeyNew(8, 1) + // save the state code + err = l.svcCtx.Redis.Set(l.ctx, fmt.Sprintf("apple:%s", code), req.Redirect, 5*60*time.Second).Err() + if err != nil { + l.Errorw("error save state code to redis: %v", logger.Field("code", code), logger.Field("error", err.Error())) + } + return fmt.Sprintf(uri, cfg.ClientId, fmt.Sprintf("%s/v1/auth/oauth/callback/apple", cfg.RedirectURL), code), nil +} +func (l *BindOAuthLogic) github() (string, error) { + return "", nil +} diff --git a/internal/logic/public/user/bindTelegramLogic.go b/internal/logic/public/user/bindTelegramLogic.go new file mode 100644 index 0000000..5fefe08 --- /dev/null +++ b/internal/logic/public/user/bindTelegramLogic.go @@ -0,0 +1,34 @@ +package user + +import ( + "context" + "fmt" + "time" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type BindTelegramLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Bind Telegram +func NewBindTelegramLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindTelegramLogic { + return &BindTelegramLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *BindTelegramLogic) BindTelegram() (resp *types.BindTelegramResponse, err error) { + session := l.ctx.Value("session").(string) + return &types.BindTelegramResponse{ + Url: fmt.Sprintf("https://t.me/%s?start=%s", l.svcCtx.Config.Telegram.BotName, session), + ExpiredAt: time.Now().Add(300 * time.Second).UnixMilli(), + }, nil +} diff --git a/internal/logic/public/user/calculateRemainingAmount.go b/internal/logic/public/user/calculateRemainingAmount.go new file mode 100644 index 0000000..9559956 --- /dev/null +++ b/internal/logic/public/user/calculateRemainingAmount.go @@ -0,0 +1,68 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/deduction" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +func CalculateRemainingAmount(ctx context.Context, svcCtx *svc.ServiceContext, userSubscribeId int64) (int64, error) { + // Find User Subscribe + userSubscribe, err := svcCtx.UserModel.FindOneUserSubscribe(ctx, userSubscribeId) + if err != nil { + logger.WithContext(ctx).Error("[func CalculateRemainingAmount(ctx context.Context, svcCtx *svc.ServiceContext, userSubscribeId int64) (int64, error) {\n] FindOneUserSubscribe", logger.Field("err", err.Error()), logger.Field("id", userSubscribeId)) + return 0, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneUserSubscribe failed, id: %d", userSubscribeId) + } + if userSubscribe.OrderId == 0 { + return 0, nil + } + if !*userSubscribe.Subscribe.AllowDeduction && !svcCtx.Config.Subscribe.SingleModel { + return 0, errors.New("The subscription package does not support deductions") + } + + if userSubscribe.Status != 1 { + return 0, errors.New("The subscription package is not in use") + } + // Find Order Details + orderDetails, err := svcCtx.OrderModel.FindOneDetails(ctx, userSubscribe.OrderId) + if err != nil { + logger.WithContext(ctx).Error("[PreUnsubscribe] FindOneDetails", logger.Field("err", err.Error()), logger.Field("id", userSubscribe.OrderId)) + return 0, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneDetails failed, id: %d", userSubscribe.OrderId) + } + // Calculate Order Quantity + orderQuantity := orderDetails.Quantity + // Calculate Order Amount + orderAmount := orderDetails.Amount + orderDetails.GiftAmount + if len(orderDetails.SubOrders) > 0 { + for _, subOrder := range orderDetails.SubOrders { + if subOrder.Status == 2 || subOrder.Status == 5 { + orderAmount += subOrder.Amount + subOrder.GiftAmount + orderQuantity += subOrder.Quantity + } + } + } + // Calculate Remaining Amount + remainingAmount := deduction.CalculateRemainingAmount( + deduction.Subscribe{ + StartTime: userSubscribe.StartTime, + ExpireTime: userSubscribe.ExpireTime, + Traffic: userSubscribe.Traffic, + Download: userSubscribe.Download, + Upload: userSubscribe.Upload, + UnitTime: userSubscribe.Subscribe.UnitTime, + UnitPrice: userSubscribe.Subscribe.UnitPrice, + ResetCycle: userSubscribe.Subscribe.ResetCycle, + DeductionRatio: userSubscribe.Subscribe.DeductionRatio, + }, + deduction.Order{ + Amount: orderAmount, + Quantity: orderQuantity, + }, + ) + return remainingAmount, nil +} diff --git a/internal/logic/public/user/getLoginLogLogic.go b/internal/logic/public/user/getLoginLogLogic.go new file mode 100644 index 0000000..e498911 --- /dev/null +++ b/internal/logic/public/user/getLoginLogLogic.go @@ -0,0 +1,51 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetLoginLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Login Log +func NewGetLoginLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetLoginLogLogic { + return &GetLoginLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetLoginLogLogic) GetLoginLog(req *types.GetLoginLogRequest) (resp *types.GetLoginLogResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + data, total, err := l.svcCtx.UserModel.FilterLoginLogList(l.ctx, req.Page, req.Size, &user.LoginLogFilterParams{ + UserId: u.Id, + }) + if err != nil { + l.Errorw("find login log failed:", logger.Field("error", err.Error()), logger.Field("user_id", u.Id)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find login log failed: %v", err.Error()) + } + list := make([]types.UserLoginLog, 0) + tool.DeepCopy(&list, data) + return &types.GetLoginLogResponse{ + Total: total, + List: list, + }, nil +} diff --git a/internal/logic/public/user/getOAuthMethodsLogic.go b/internal/logic/public/user/getOAuthMethodsLogic.go new file mode 100644 index 0000000..3ad0f2b --- /dev/null +++ b/internal/logic/public/user/getOAuthMethodsLogic.go @@ -0,0 +1,48 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetOAuthMethodsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get OAuth Methods +func NewGetOAuthMethodsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetOAuthMethodsLogic { + return &GetOAuthMethodsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetOAuthMethodsLogic) GetOAuthMethods() (resp *types.GetOAuthMethodsResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + methods, err := l.svcCtx.UserModel.FindUserAuthMethods(l.ctx, u.Id) + if err != nil { + l.Errorw("find user auth methods failed:", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user auth methods failed: %v", err.Error()) + } + list := make([]types.UserAuthMethod, 0) + tool.DeepCopy(&list, methods) + return &types.GetOAuthMethodsResponse{ + Methods: list, + }, nil +} diff --git a/internal/logic/public/user/getSubscribeLogLogic.go b/internal/logic/public/user/getSubscribeLogLogic.go new file mode 100644 index 0000000..b495c7a --- /dev/null +++ b/internal/logic/public/user/getSubscribeLogLogic.go @@ -0,0 +1,52 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetSubscribeLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Subscribe Log +func NewGetSubscribeLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSubscribeLogLogic { + return &GetSubscribeLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetSubscribeLogLogic) GetSubscribeLog(req *types.GetSubscribeLogRequest) (resp *types.GetSubscribeLogResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + data, total, err := l.svcCtx.UserModel.FilterSubscribeLogList(l.ctx, req.Page, req.Size, &user.SubscribeLogFilterParams{ + UserId: u.Id, + }) + if err != nil { + l.Errorw("[GetUserSubscribeLogs] Get User Subscribe Logs Error:", logger.Field("err", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Get User Subscribe Logs Error") + } + var list []types.UserSubscribeLog + tool.DeepCopy(&list, data) + + return &types.GetSubscribeLogResponse{ + List: list, + Total: total, + }, err +} diff --git a/internal/logic/public/user/preUnsubscribeLogic.go b/internal/logic/public/user/preUnsubscribeLogic.go new file mode 100644 index 0000000..2554a4a --- /dev/null +++ b/internal/logic/public/user/preUnsubscribeLogic.go @@ -0,0 +1,35 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type PreUnsubscribeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewPreUnsubscribeLogic Pre Unsubscribe +func NewPreUnsubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PreUnsubscribeLogic { + return &PreUnsubscribeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *PreUnsubscribeLogic) PreUnsubscribe(req *types.PreUnsubscribeRequest) (resp *types.PreUnsubscribeResponse, err error) { + remainingAmount, err := CalculateRemainingAmount(l.ctx, l.svcCtx, req.Id) + if err != nil { + l.Errorw("[PreUnsubscribeLogic] Calculate Remaining Amount Error:", logger.Field("err", err.Error())) + return nil, err + } + return &types.PreUnsubscribeResponse{ + DeductionAmount: remainingAmount, + }, nil +} diff --git a/internal/logic/public/user/queryUserAffiliateListLogic.go b/internal/logic/public/user/queryUserAffiliateListLogic.go new file mode 100644 index 0000000..5182b77 --- /dev/null +++ b/internal/logic/public/user/queryUserAffiliateListLogic.go @@ -0,0 +1,84 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type QueryUserAffiliateListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Query User Affiliate List +func NewQueryUserAffiliateListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserAffiliateListLogic { + return &QueryUserAffiliateListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryUserAffiliateListLogic) QueryUserAffiliateList(req *types.QueryUserAffiliateListRequest) (resp *types.QueryUserAffiliateListResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + var data []*user.User + var total int64 + err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + return db.Model(&user.User{}).Order("id desc").Where("referer_id = ?", u.Id).Count(&total).Limit(req.Size).Offset((req.Page - 1) * req.Size).Find(&data).Error + }) + if err != nil { + l.Errorw("Query User Affiliate List failed: %v", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Affiliate List failed: %v", err.Error()) + } + + list := make([]types.UserAffiliate, 0) + for _, item := range data { + list = append(list, types.UserAffiliate{ + Identifier: GetAuthMethod(l, item).AuthIdentifier, + Avatar: item.Avatar, + RegisteredAt: item.CreatedAt.UnixMilli(), + Enable: *item.Enable, + }) + } + return &types.QueryUserAffiliateListResponse{ + Total: total, + List: list, + }, nil +} + +func GetAuthMethod(l *QueryUserAffiliateListLogic, item *user.User) user.AuthMethods { + authMethod := user.AuthMethods{} + authMethods, errs := l.svcCtx.UserModel.FindUserAuthMethods(l.ctx, item.Id) + if errs == nil && len(authMethods) > 0 { + for _, am := range authMethods { + if am.AuthType == "6" || am.AuthType == "7" { + authMethod = *am + break + } + } + if authMethod.AuthIdentifier == "" { + authMethod = *authMethods[0] + } + + hideTextLength := len(authMethod.AuthIdentifier) / 3 + if hideTextLength > 0 { + authMethod.AuthIdentifier = authMethod.AuthIdentifier[0:hideTextLength] + "***" + authMethod.AuthIdentifier[hideTextLength*2:] + } + } + return authMethod +} diff --git a/internal/logic/public/user/queryUserAffiliateLogic.go b/internal/logic/public/user/queryUserAffiliateLogic.go new file mode 100644 index 0000000..5240fc9 --- /dev/null +++ b/internal/logic/public/user/queryUserAffiliateLogic.go @@ -0,0 +1,61 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type QueryUserAffiliateLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Query User Balance Log +func NewQueryUserAffiliateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserAffiliateLogic { + return &QueryUserAffiliateLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryUserAffiliateLogic) QueryUserAffiliate() (resp *types.QueryUserAffiliateCountResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + var sum int64 + var total int64 + err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + return db.Model(&user.User{}).Where("referer_id = ?", u.Id).Count(&total).Find(&user.User{}).Error + }) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Affiliate failed: %v", err) + } + err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + return db.Model(&user.CommissionLog{}). + Where("user_id = ?", u.Id). + Select("COALESCE(SUM(amount), 0)"). + Scan(&sum).Error + }) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Affiliate failed: %v", err) + } + + return &types.QueryUserAffiliateCountResponse{ + Registers: total, + TotalCommission: sum, + }, nil +} diff --git a/internal/logic/public/user/queryUserBalanceLogLogic.go b/internal/logic/public/user/queryUserBalanceLogLogic.go new file mode 100644 index 0000000..a48f267 --- /dev/null +++ b/internal/logic/public/user/queryUserBalanceLogLogic.go @@ -0,0 +1,55 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type QueryUserBalanceLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Query User Balance Log +func NewQueryUserBalanceLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserBalanceLogLogic { + return &QueryUserBalanceLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryUserBalanceLogLogic) QueryUserBalanceLog() (resp *types.QueryUserBalanceLogListResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + var data []*user.BalanceLog + var total int64 + // Query User Balance Log + err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + return db.Model(&user.BalanceLog{}).Order("created_at DESC").Where("user_id = ?", u.Id).Count(&total).Find(&data).Error + }) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Balance Log failed: %v", err) + } + resp = &types.QueryUserBalanceLogListResponse{ + List: make([]types.UserBalanceLog, 0), + Total: total, + } + tool.DeepCopy(&resp.List, data) + return +} diff --git a/internal/logic/public/user/queryUserCommissionLogLogic.go b/internal/logic/public/user/queryUserCommissionLogLogic.go new file mode 100644 index 0000000..f4c81e7 --- /dev/null +++ b/internal/logic/public/user/queryUserCommissionLogLogic.go @@ -0,0 +1,53 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type QueryUserCommissionLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Query User Commission Log +func NewQueryUserCommissionLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserCommissionLogLogic { + return &QueryUserCommissionLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryUserCommissionLogLogic) QueryUserCommissionLog(req *types.QueryUserCommissionLogListRequest) (resp *types.QueryUserCommissionLogListResponse, err error) { + var data []*user.CommissionLog + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + return db.Order("id desc").Limit(req.Size).Offset((req.Page-1)*req.Size).Where("user_id = ?", u.Id).Find(&data).Error + }) + if err != nil { + l.Errorw("Query User Commission Log failed", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Commission Log failed: %v", err) + } + var list []types.CommissionLog + tool.DeepCopy(&list, data) + return &types.QueryUserCommissionLogListResponse{ + List: list, + }, nil +} diff --git a/internal/logic/public/user/queryUserInfoLogic.go b/internal/logic/public/user/queryUserInfoLogic.go new file mode 100644 index 0000000..8cbd54a --- /dev/null +++ b/internal/logic/public/user/queryUserInfoLogic.go @@ -0,0 +1,76 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/phone" + "github.com/perfect-panel/ppanel-server/pkg/tool" +) + +type QueryUserInfoLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Query User Info +func NewQueryUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserInfoLogic { + return &QueryUserInfoLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryUserInfoLogic) QueryUserInfo() (resp *types.User, err error) { + resp = &types.User{} + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + tool.DeepCopy(resp, u) + + var userMethods []types.UserAuthMethod + for _, method := range resp.AuthMethods { + var item types.UserAuthMethod + tool.DeepCopy(&item, method) + + switch method.AuthType { + case "mobile": + item.AuthIdentifier = phone.MaskPhoneNumber(method.AuthIdentifier) + case "email": + default: + item.AuthIdentifier = maskOpenID(method.AuthIdentifier) + } + userMethods = append(userMethods, item) + } + resp.AuthMethods = userMethods + return resp, nil +} + +// maskOpenID 脱敏 OpenID,只保留前 3 和后 3 位 +func maskOpenID(openID string) string { + length := len(openID) + if length <= 6 { + return "***" // 如果 ID 太短,直接返回 "***" + } + + // 计算中间需要被替换的 `*` 数量 + maskLength := length - 6 + mask := make([]byte, maskLength) + for i := range mask { + mask[i] = '*' + } + + // 组合脱敏后的 OpenID + return openID[:3] + string(mask) + openID[length-3:] +} diff --git a/internal/logic/public/user/queryUserSubscribeLogic.go b/internal/logic/public/user/queryUserSubscribeLogic.go new file mode 100644 index 0000000..372e19b --- /dev/null +++ b/internal/logic/public/user/queryUserSubscribeLogic.go @@ -0,0 +1,83 @@ +package user + +import ( + "context" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type QueryUserSubscribeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Query User Subscribe +func NewQueryUserSubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserSubscribeLogic { + return &QueryUserSubscribeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryUserSubscribeLogic) QueryUserSubscribe() (resp *types.QueryUserSubscribeListResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + data, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id, 1, 0) + if err != nil { + l.Errorw("[QueryUserSubscribeLogic] Query User Subscribe Error:", logger.Field("err", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Subscribe Error") + } + + resp = &types.QueryUserSubscribeListResponse{ + List: make([]types.UserSubscribe, 0), + Total: int64(len(data)), + } + + for _, item := range data { + var sub types.UserSubscribe + tool.DeepCopy(&sub, item) + sub.ResetTime = calculateNextResetTime(&sub) + resp.List = append(resp.List, sub) + } + return +} + +// 计算下次重置时间 +func calculateNextResetTime(sub *types.UserSubscribe) int64 { + startTime := time.UnixMilli(sub.StartTime) + now := time.Now() + switch sub.Subscribe.ResetCycle { + case 0: + return 0 + case 1: + return time.Date(now.Year(), now.Month()+1, 1, 0, 0, 0, 0, now.Location()).UnixMilli() + case 2: + if startTime.Day() > now.Day() { + return time.Date(now.Year(), now.Month(), startTime.Day(), 0, 0, 0, 0, now.Location()).UnixMilli() + } else { + return time.Date(now.Year(), now.Month()+1, startTime.Day(), 0, 0, 0, 0, now.Location()).UnixMilli() + } + case 3: + targetTime := time.Date(now.Year(), startTime.Month(), startTime.Day(), 0, 0, 0, 0, now.Location()) + if targetTime.Before(now) { + targetTime = time.Date(now.Year()+1, startTime.Month(), startTime.Day(), 0, 0, 0, 0, now.Location()) + } + return targetTime.UnixMilli() + default: + return 0 + } +} diff --git a/internal/logic/public/user/resetUserSubscribeTokenLogic.go b/internal/logic/public/user/resetUserSubscribeTokenLogic.go new file mode 100644 index 0000000..30bfd9e --- /dev/null +++ b/internal/logic/public/user/resetUserSubscribeTokenLogic.go @@ -0,0 +1,67 @@ +package user + +import ( + "context" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/google/uuid" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/uuidx" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type ResetUserSubscribeTokenLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewResetUserSubscribeTokenLogic Reset User Subscribe Token +func NewResetUserSubscribeTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetUserSubscribeTokenLogic { + return &ResetUserSubscribeTokenLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ResetUserSubscribeTokenLogic) ResetUserSubscribeToken(req *types.ResetUserSubscribeTokenRequest) error { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + userSub, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeId) + if err != nil { + l.Errorw("FindOneUserSubscribe failed:", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneUserSubscribe failed: %v", err.Error()) + } + if userSub.UserId != u.Id { + l.Errorw("UserSubscribeId does not belong to the current user") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "UserSubscribeId does not belong to the current user") + } + // find order + orderDetails, err := l.svcCtx.OrderModel.FindOneDetails(l.ctx, userSub.OrderId) + if err != nil { + l.Errorw("FindOneDetails failed:", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneDetails failed: %v", err.Error()) + } + userSub.Token = uuidx.SubscribeToken(orderDetails.OrderNo + time.Now().Format("20060102150405.000")) + userSub.UUID = uuid.New().String() + var newSub user.Subscribe + tool.DeepCopy(&newSub, userSub) + + err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, &newSub) + if err != nil { + l.Errorw("UpdateSubscribe failed:", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateSubscribe failed: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/public/user/unbindOAuthLogic.go b/internal/logic/public/user/unbindOAuthLogic.go new file mode 100644 index 0000000..97887a8 --- /dev/null +++ b/internal/logic/public/user/unbindOAuthLogic.go @@ -0,0 +1,49 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type UnbindOAuthLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Unbind OAuth +func NewUnbindOAuthLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UnbindOAuthLogic { + return &UnbindOAuthLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UnbindOAuthLogic) UnbindOAuth(req *types.UnbindOAuthRequest) error { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + if !l.validator(req) { + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "invalid parameter") + } + err := l.svcCtx.UserModel.DeleteUserAuthMethods(l.ctx, u.Id, req.Method) + if err != nil { + l.Errorw("delete user auth methods failed:", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete user auth methods failed: %v", err.Error()) + } + return nil +} +func (l *UnbindOAuthLogic) validator(req *types.UnbindOAuthRequest) bool { + return req.Method != "" && req.Method != "email" && req.Method != "mobile" +} diff --git a/internal/logic/public/user/unbindTelegramLogic.go b/internal/logic/public/user/unbindTelegramLogic.go new file mode 100644 index 0000000..e379614 --- /dev/null +++ b/internal/logic/public/user/unbindTelegramLogic.go @@ -0,0 +1,81 @@ +package user + +import ( + "context" + "strconv" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/perfect-panel/ppanel-server/internal/logic/telegram" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type UnbindTelegramLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Unbind Telegram +func NewUnbindTelegramLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UnbindTelegramLogic { + return &UnbindTelegramLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UnbindTelegramLogic) UnbindTelegram() error { + // Get User Info + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + + if !ok { + logger.Error("current user is not found in context") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + method, err := l.svcCtx.UserModel.FindUserAuthMethodByPlatform(l.ctx, u.Id, "telegram") + if err != nil { + l.Errorw("UnbindTelegramLogic FindUserAuthMethodByPlatform Error", logger.Field("id", u.Id), logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Find User Auth Method By Platform Failed") + } + + userTelegramChatId, err := strconv.ParseInt(method.AuthIdentifier, 10, 64) + if err != nil { + l.Errorw("UnbindTelegramLogic ParseInt Error", logger.Field("id", u.Id), logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ParseInt Error") + } + + if userTelegramChatId == 0 { + return errors.Wrapf(xerr.NewErrCode(xerr.TelegramNotBound), "Unbind Telegram") + } + + // Unbind Telegram + err = l.svcCtx.UserModel.DeleteUserAuthMethods(l.ctx, u.Id, "telegram") + if err != nil { + l.Errorw("UnbindTelegramLogic DeleteUserAuthMethods Error", logger.Field("id", u.Id), logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "Delete User Auth Methods Failed") + } + // Unbind Telegram Success send message with chatId + text, err := tool.RenderTemplateToString(telegram.UnbindNotify, map[string]string{ + "Id": strconv.FormatInt(u.Id, 10), + "Time": time.Now().Format("2006-01-02 15:04:05"), + }) + if err != nil { + l.Errorw("UnbindTelegramLogic RenderTemplateToString Error", logger.Field("id", u.Id), logger.Field("error", err.Error())) + return nil + } + msg := tgbotapi.NewMessage(userTelegramChatId, text) + _, err = l.svcCtx.TelegramBot.Send(msg) + if err != nil { + l.Errorw("UnbindTelegramLogic Send Error", logger.Field("id", u.Id), logger.Field("error", err.Error())) + return nil + } + return nil +} diff --git a/internal/logic/public/user/unsubscribeLogic.go b/internal/logic/public/user/unsubscribeLogic.go new file mode 100644 index 0000000..0befd5d --- /dev/null +++ b/internal/logic/public/user/unsubscribeLogic.go @@ -0,0 +1,71 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type UnsubscribeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewUnsubscribeLogic Unsubscribe +func NewUnsubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UnsubscribeLogic { + return &UnsubscribeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UnsubscribeLogic) Unsubscribe(req *types.UnsubscribeRequest) error { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + remainingAmount, err := CalculateRemainingAmount(l.ctx, l.svcCtx, req.Id) + if err != nil { + return err + } + // update user subscribe + err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + var userSub user.Subscribe + if err := db.Model(&user.Subscribe{}).Where("id = ?", req.Id).First(&userSub).Error; err != nil { + return err + } + userSub.Status = 4 + if err := l.svcCtx.UserModel.UpdateSubscribe(l.ctx, &userSub); err != nil { + return err + } + balance := remainingAmount + u.Balance + // insert deduction log + balanceLog := user.BalanceLog{ + UserId: userSub.UserId, + OrderId: userSub.OrderId, + Amount: remainingAmount, + Type: 4, + Balance: balance, + } + if err := db.Model(&user.BalanceLog{}).Create(&balanceLog).Error; err != nil { + return err + } + // update user balance + u.Balance = balance + return l.svcCtx.UserModel.Update(l.ctx, u) + }) + + return err +} diff --git a/internal/logic/public/user/updateBindEmailLogic.go b/internal/logic/public/user/updateBindEmailLogic.go new file mode 100644 index 0000000..3640f90 --- /dev/null +++ b/internal/logic/public/user/updateBindEmailLogic.go @@ -0,0 +1,69 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type UpdateBindEmailLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewUpdateBindEmailLogic Update Bind Email +func NewUpdateBindEmailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateBindEmailLogic { + return &UpdateBindEmailLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateBindEmailLogic) UpdateBindEmail(req *types.UpdateBindEmailRequest) error { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + method, err := l.svcCtx.UserModel.FindUserAuthMethodByUserId(l.ctx, "email", u.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindUserAuthMethodByOpenID error") + } + m, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "email", req.Email) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindUserAuthMethodByOpenID error") + } + // email already bind + if m.Id > 0 { + return errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "email already bind") + } + if errors.Is(err, gorm.ErrRecordNotFound) { + method = &user.AuthMethods{ + UserId: u.Id, + AuthType: "email", + AuthIdentifier: req.Email, + Verified: false, + } + if err := l.svcCtx.UserModel.InsertUserAuthMethods(l.ctx, method); err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "InsertUserAuthMethods error") + } + } else { + method.Verified = false + method.AuthIdentifier = req.Email + if err := l.svcCtx.UserModel.UpdateUserAuthMethods(l.ctx, method); err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateUserAuthMethods error") + } + } + return nil +} diff --git a/internal/logic/public/user/updateBindMobileLogic.go b/internal/logic/public/user/updateBindMobileLogic.go new file mode 100644 index 0000000..61e100e --- /dev/null +++ b/internal/logic/public/user/updateBindMobileLogic.go @@ -0,0 +1,94 @@ +package user + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/phone" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type UpdateBindMobileLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Update Bind Mobile +func NewUpdateBindMobileLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateBindMobileLogic { + return &UpdateBindMobileLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateBindMobileLogic) UpdateBindMobile(req *types.UpdateBindMobileRequest) error { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + // verify mobile + phoneNumber, err := phone.FormatToE164(req.AreaCode, req.Mobile) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number") + } + cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.Register, phoneNumber) + code, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() + if err != nil { + l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) + return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + var payload CacheKeyPayload + err = json.Unmarshal([]byte(code), &payload) + if err != nil { + l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) + return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + if payload.Code != req.Code { + return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + l.svcCtx.Redis.Del(l.ctx, cacheKey) + + m, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "mobile", req.Mobile) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindUserAuthMethodByOpenID error") + } + if m.Id > 0 { + return errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "mobile already bind") + } + + method, err := l.svcCtx.UserModel.FindUserAuthMethodByUserId(l.ctx, "mobile", u.Id) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindUserAuthMethodByOpenID error") + } + if errors.Is(err, gorm.ErrRecordNotFound) { + method = &user.AuthMethods{ + UserId: u.Id, + AuthType: "mobile", + AuthIdentifier: req.Mobile, + Verified: true, + } + if err := l.svcCtx.UserModel.InsertUserAuthMethods(l.ctx, method); err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "InsertUserAuthMethods error") + } + } else { + method.Verified = true + method.AuthIdentifier = req.Mobile + if err := l.svcCtx.UserModel.UpdateUserAuthMethods(l.ctx, method); err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateUserAuthMethods error") + } + } + return nil +} diff --git a/internal/logic/public/user/updateUserNotifyLogic.go b/internal/logic/public/user/updateUserNotifyLogic.go new file mode 100644 index 0000000..49ca2e6 --- /dev/null +++ b/internal/logic/public/user/updateUserNotifyLogic.go @@ -0,0 +1,48 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateUserNotifyLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Update User Notify +func NewUpdateUserNotifyLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserNotifyLogic { + return &UpdateUserNotifyLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateUserNotifyLogic) UpdateUserNotify(req *types.UpdateUserNotifyRequest) error { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + if u.Id == 0 { + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "user not login") + } + u.EnableLoginNotify = req.EnableLoginNotify + u.EnableBalanceNotify = req.EnableBalanceNotify + u.EnableSubscribeNotify = req.EnableSubscribeNotify + u.EnableTradeNotify = req.EnableTradeNotify + if err := l.svcCtx.UserModel.Update(l.ctx, u); err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "update user notify error: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/public/user/updateUserPasswordLogic.go b/internal/logic/public/user/updateUserPasswordLogic.go new file mode 100644 index 0000000..265f95e --- /dev/null +++ b/internal/logic/public/user/updateUserPasswordLogic.go @@ -0,0 +1,41 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type UpdateUserPasswordLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Update User Password +func NewUpdateUserPasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserPasswordLogic { + return &UpdateUserPasswordLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateUserPasswordLogic) UpdateUserPassword(req *types.UpdateUserPasswordRequest) error { + userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) + //update the password + userInfo.Password = tool.EncodePassWord(req.Password) + if err := l.svcCtx.UserModel.Update(l.ctx, userInfo); err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Update user password error") + } + return nil +} diff --git a/internal/logic/public/user/verifyEmailLogic.go b/internal/logic/public/user/verifyEmailLogic.go new file mode 100644 index 0000000..1831c84 --- /dev/null +++ b/internal/logic/public/user/verifyEmailLogic.go @@ -0,0 +1,75 @@ +package user + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +type VerifyEmailLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Verify Email +func NewVerifyEmailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *VerifyEmailLogic { + return &VerifyEmailLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +type CacheKeyPayload struct { + Code string `json:"code"` + LastAt int64 `json:"lastAt"` +} + +func (l *VerifyEmailLogic) VerifyEmail(req *types.VerifyEmailRequest) error { + cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Security, req.Email) + value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() + if err != nil { + l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) + return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + + var payload CacheKeyPayload + err = json.Unmarshal([]byte(value), &payload) + if err != nil { + l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) + return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + if payload.Code != req.Code { + return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + } + l.svcCtx.Redis.Del(l.ctx, cacheKey) + + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + method, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "email", req.Email) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindUserAuthMethodByOpenID error") + } + if method.UserId != u.Id { + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "invalid access") + } + method.Verified = true + err = l.svcCtx.UserModel.UpdateUserAuthMethods(l.ctx, method) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateUserAuthMethods error") + } + return nil +} diff --git a/internal/logic/server/constant.go b/internal/logic/server/constant.go new file mode 100644 index 0000000..0c54202 --- /dev/null +++ b/internal/logic/server/constant.go @@ -0,0 +1,3 @@ +package server + +const Unchanged = "Unchanged" diff --git a/internal/logic/server/getServerConfigLogic.go b/internal/logic/server/getServerConfigLogic.go new file mode 100644 index 0000000..1b57ee7 --- /dev/null +++ b/internal/logic/server/getServerConfigLogic.go @@ -0,0 +1,83 @@ +package server + +import ( + "encoding/json" + "fmt" + + "github.com/gin-gonic/gin" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" +) + +type GetServerConfigLogic struct { + logger.Logger + ctx *gin.Context + svcCtx *svc.ServiceContext +} + +// Get server config +func NewGetServerConfigLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *GetServerConfigLogic { + return &GetServerConfigLogic{ + Logger: logger.WithContext(ctx.Request.Context()), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetServerConfigLogic) GetServerConfig(req *types.GetServerConfigRequest) (resp *types.GetServerConfigResponse, err error) { + cacheKey := fmt.Sprintf("%s%d", config.ServerConfigCacheKey, req.ServerId) + cache, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() + if err == nil { + if cache != "" { + etag := tool.GenerateETag([]byte(cache)) + // Check If-None-Match header + match := l.ctx.GetHeader("If-None-Match") + if match == etag { + return nil, xerr.StatusNotModified + } + l.ctx.Header("ETag", etag) + resp := &types.GetServerConfigResponse{} + err = json.Unmarshal([]byte(cache), resp) + if err != nil { + l.Errorw("[ServerConfigCacheKey] json unmarshal error", logger.Field("error", err.Error())) + return nil, err + } + return resp, nil + } + } + nodeInfo, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.ServerId) + if err != nil { + l.Errorw("[GetServerConfig] FindOne error", logger.Field("error", err.Error())) + return nil, err + } + cfg := make(map[string]interface{}) + err = json.Unmarshal([]byte(nodeInfo.Config), &cfg) + if err != nil { + l.Errorw("[GetServerConfig] json unmarshal error", logger.Field("error", err.Error())) + return nil, err + } + resp = &types.GetServerConfigResponse{ + Basic: types.ServerBasic{ + PullInterval: l.svcCtx.Config.Node.NodePullInterval, + PushInterval: l.svcCtx.Config.Node.NodePushInterval, + }, + Protocol: nodeInfo.Protocol, + Config: cfg, + } + data, err := json.Marshal(resp) + if err != nil { + l.Errorw("[GetServerConfig] json marshal error", logger.Field("error", err.Error())) + return nil, err + } + etag := tool.GenerateETag(data) + l.ctx.Header("ETag", etag) + if err = l.svcCtx.Redis.Set(l.ctx, cacheKey, data, -1).Err(); err != nil { + l.Errorw("[GetServerConfig] redis set error", logger.Field("error", err.Error())) + } + return resp, nil +} diff --git a/internal/logic/server/getServerUserListLogic.go b/internal/logic/server/getServerUserListLogic.go new file mode 100644 index 0000000..723e648 --- /dev/null +++ b/internal/logic/server/getServerUserListLogic.go @@ -0,0 +1,110 @@ +package server + +import ( + "encoding/json" + "fmt" + + "github.com/gin-gonic/gin" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/uuidx" + "github.com/perfect-panel/ppanel-server/pkg/xerr" +) + +type GetServerUserListLogic struct { + logger.Logger + ctx *gin.Context + svcCtx *svc.ServiceContext +} + +// Get user list +func NewGetServerUserListLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *GetServerUserListLogic { + return &GetServerUserListLogic{ + Logger: logger.WithContext(ctx.Request.Context()), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListRequest) (resp *types.GetServerUserListResponse, err error) { + cacheKey := fmt.Sprintf("%s%d", config.ServerUserListCacheKey, req.ServerId) + cache, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() + if err == nil { + if cache != "" { + etag := tool.GenerateETag([]byte(cache)) + resp := &types.GetServerUserListResponse{} + // Check If-None-Match header + if match := l.ctx.GetHeader("If-None-Match"); match == etag { + return nil, xerr.StatusNotModified + } + l.ctx.Header("ETag", etag) + err = json.Unmarshal([]byte(cache), resp) + if err != nil { + l.Errorw("[ServerUserListCacheKey] json unmarshal error", logger.Field("error", err.Error())) + return nil, err + } + return resp, nil + } + } + server, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.ServerId) + if err != nil { + return nil, err + } + subs, err := l.svcCtx.SubscribeModel.QuerySubscribeIdsByServerIdAndServerGroupId(l.ctx, server.Id, server.GroupId) + if err != nil { + l.Errorw("QuerySubscribeIdsByServerIdAndServerGroupId error", logger.Field("error", err.Error())) + return nil, err + } + if len(subs) == 0 { + return &types.GetServerUserListResponse{ + Users: []types.ServerUser{ + { + Id: 1, + UUID: uuidx.NewUUID().String(), + }, + }, + }, nil + } + users := make([]types.ServerUser, 0) + for _, sub := range subs { + data, err := l.svcCtx.UserModel.FindUsersSubscribeBySubscribeId(l.ctx, sub.Id) + if err != nil { + return nil, err + } + for _, datum := range data { + speedLimit := server.SpeedLimit + if (int(sub.SpeedLimit) < server.SpeedLimit && sub.SpeedLimit != 0) || + (int(sub.SpeedLimit) > server.SpeedLimit && sub.SpeedLimit == 0) { + speedLimit = int(sub.SpeedLimit) + } + + users = append(users, types.ServerUser{ + Id: datum.Id, + UUID: datum.UUID, + SpeedLimit: int64(speedLimit), + DeviceLimit: sub.DeviceLimit, + }) + } + } + if len(users) == 0 { + users = append(users, types.ServerUser{ + Id: 1, + UUID: uuidx.NewUUID().String(), + }) + } + resp = &types.GetServerUserListResponse{ + Users: users, + } + val, _ := json.Marshal(resp) + etag := tool.GenerateETag(val) + l.ctx.Header("ETag", etag) + err = l.svcCtx.Redis.Set(l.ctx, cacheKey, string(val), -1).Err() + if err != nil { + l.Errorw("[ServerUserListCacheKey] redis set error", logger.Field("error", err.Error())) + } + return resp, nil +} diff --git a/internal/logic/server/pushOnlineUsersLogic.go b/internal/logic/server/pushOnlineUsersLogic.go new file mode 100644 index 0000000..acdb942 --- /dev/null +++ b/internal/logic/server/pushOnlineUsersLogic.go @@ -0,0 +1,70 @@ +package server + +import ( + "context" + "errors" + "fmt" + + "github.com/perfect-panel/ppanel-server/internal/model/cache" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type PushOnlineUsersLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewPushOnlineUsersLogic Push online users +func NewPushOnlineUsersLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PushOnlineUsersLogic { + return &PushOnlineUsersLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *PushOnlineUsersLogic) PushOnlineUsers(req *types.OnlineUsersRequest) error { + // 验证请求数据 + if req.ServerId <= 0 || len(req.Users) == 0 { + return errors.New("invalid request parameters") + } + + // 验证用户数据 + for _, user := range req.Users { + if user.SID <= 0 || user.IP == "" { + return fmt.Errorf("invalid user data: uid=%d, ip=%s", user.SID, user.IP) + } + } + + // Find server info + _, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.ServerId) + if err != nil { + l.Errorw("[PushOnlineUsers] FindOne error", logger.Field("error", err)) + return fmt.Errorf("server not found: %w", err) + } + + userOnlineIp := make([]cache.NodeOnlineUser, 0) + for _, user := range req.Users { + userOnlineIp = append(userOnlineIp, cache.NodeOnlineUser{ + SID: user.SID, + IP: user.IP, + }) + } + err = l.svcCtx.NodeCache.AddOnlineUserIP(l.ctx, userOnlineIp) + if err != nil { + l.Errorw("[PushOnlineUsers] cache operation error", logger.Field("error", err)) + return err + } + + err = l.svcCtx.NodeCache.UpdateNodeOnlineUser(l.ctx, req.ServerId, userOnlineIp) + + if err != nil { + l.Errorw("[PushOnlineUsers] cache operation error", logger.Field("error", err)) + return err + } + + return nil +} diff --git a/internal/logic/server/serverPushStatusLogic.go b/internal/logic/server/serverPushStatusLogic.go new file mode 100644 index 0000000..e1563d8 --- /dev/null +++ b/internal/logic/server/serverPushStatusLogic.go @@ -0,0 +1,46 @@ +package server + +import ( + "context" + "errors" + + "github.com/perfect-panel/ppanel-server/internal/model/cache" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type ServerPushStatusLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Push server status +func NewServerPushStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ServerPushStatusLogic { + return &ServerPushStatusLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ServerPushStatusLogic) ServerPushStatus(req *types.ServerPushStatusRequest) error { + // Find server info + serverInfo, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.ServerId) + if err != nil || serverInfo.Id <= 0 { + l.Errorw("[PushOnlineUsers] FindOne error", logger.Field("error", err)) + return errors.New("server not found") + } + err = l.svcCtx.NodeCache.UpdateNodeStatus(l.ctx, req.ServerId, cache.NodeStatus{ + Cpu: req.Cpu, + Mem: req.Mem, + Disk: req.Disk, + UpdatedAt: req.UpdatedAt, + }) + if err != nil { + l.Errorw("[ServerPushStatus] UpdateNodeStatus error", logger.Field("error", err)) + return errors.New("update node status failed") + } + return nil +} diff --git a/internal/logic/server/serverPushUserTrafficLogic.go b/internal/logic/server/serverPushUserTrafficLogic.go new file mode 100644 index 0000000..4b9461a --- /dev/null +++ b/internal/logic/server/serverPushUserTrafficLogic.go @@ -0,0 +1,70 @@ +package server + +import ( + "context" + "encoding/json" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/model/cache" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + task "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/pkg/errors" +) + +//goland:noinspection GoNameStartsWithPackageName +type ServerPushUserTrafficLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewServerPushUserTrafficLogic Push user Traffic +func NewServerPushUserTrafficLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ServerPushUserTrafficLogic { + return &ServerPushUserTrafficLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ServerPushUserTrafficLogic) ServerPushUserTraffic(req *types.ServerPushUserTrafficRequest) error { + // Find server info + serverInfo, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.ServerId) + if err != nil { + l.Errorw("[PushOnlineUsers] FindOne error", logger.Field("error", err)) + return errors.New("server not found") + } + + // Create traffic task + var request task.TrafficStatistics + var userTraffic []cache.UserTraffic + request.ServerId = serverInfo.Id + tool.DeepCopy(&request.Logs, req.Traffic) + tool.DeepCopy(&userTraffic, req.Traffic) + + // update today traffic rank + err = l.svcCtx.NodeCache.AddNodeTodayTraffic(l.ctx, serverInfo.Id, userTraffic) + if err != nil { + l.Errorw("[ServerPushUserTraffic] AddNodeTodayTraffic error", logger.Field("error", err)) + return errors.New("add node today traffic error") + } + for _, user := range req.Traffic { + if err = l.svcCtx.NodeCache.AddUserTodayTraffic(l.ctx, user.SID, user.Upload, user.Download); err != nil { + l.Errorw("[ServerPushUserTraffic] AddUserTodayTraffic error", logger.Field("error", err)) + continue + } + } + // Push traffic task + val, _ := json.Marshal(request) + t := asynq.NewTask(task.ForthwithTrafficStatistics, val, asynq.MaxRetry(3)) + info, err := l.svcCtx.Queue.EnqueueContext(l.ctx, t) + if err != nil { + l.Errorw("[ServerPushUserTraffic] Push traffic task error", logger.Field("error", err.Error()), logger.Field("task", t)) + } else { + l.Infow("[ServerPushUserTraffic] Push traffic task success", logger.Field("task", t), logger.Field("info", info)) + } + return nil +} diff --git a/internal/logic/subscribe/subscribeLogic.go b/internal/logic/subscribe/subscribeLogic.go new file mode 100644 index 0000000..36bb348 --- /dev/null +++ b/internal/logic/subscribe/subscribeLogic.go @@ -0,0 +1,289 @@ +package subscribe + +import ( + "fmt" + "strings" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/adapter" + "github.com/perfect-panel/ppanel-server/pkg/adapter/shadowrocket" + "github.com/perfect-panel/ppanel-server/pkg/adapter/surfboard" + + "github.com/perfect-panel/ppanel-server/internal/model/server" + + "github.com/perfect-panel/ppanel-server/internal/model/user" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +//goland:noinspection GoNameStartsWithPackageName +type SubscribeLogic struct { + ctx *gin.Context + svc *svc.ServiceContext + logger.Logger +} + +func NewSubscribeLogic(ctx *gin.Context, svc *svc.ServiceContext) *SubscribeLogic { + return &SubscribeLogic{ + ctx: ctx, + svc: svc, + Logger: logger.WithContext(ctx.Request.Context()), + } +} + +func (l *SubscribeLogic) Generate(req *types.SubscribeRequest) (*types.SubscribeResponse, error) { + userSub, err := l.getUserSubscribe(req.Token) + if err != nil { + return nil, err + } + + var subscribeStatus = false + defer func() { + l.logSubscribeActivity(subscribeStatus, userSub, req) + }() + + servers, err := l.getServers(userSub) + if err != nil { + return nil, err + } + + rules, err := l.getRules() + if err != nil { + return nil, err + } + + resp, headerInfo, err := l.buildClientConfig(req, userSub, servers, rules) + if err != nil { + return nil, err + } + + subscribeStatus = true + return &types.SubscribeResponse{ + Config: resp, + Header: headerInfo, + }, nil +} + +func (l *SubscribeLogic) getUserSubscribe(token string) (*user.Subscribe, error) { + userSub, err := l.svc.UserModel.FindOneSubscribeByToken(l.ctx.Request.Context(), token) + if err != nil { + l.Infow("[Generate Subscribe]find subscribe error: %v", logger.Field("error", err.Error()), logger.Field("token", token)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error()) + } + + if userSub.Status != 1 { + l.Infow("[Generate Subscribe]subscribe is not available", logger.Field("status", int(userSub.Status)), logger.Field("token", token)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeNotAvailable), "subscribe is not available") + } + + return userSub, nil +} + +func (l *SubscribeLogic) logSubscribeActivity(subscribeStatus bool, userSub *user.Subscribe, req *types.SubscribeRequest) { + if !subscribeStatus { + return + } + + err := l.svc.UserModel.InsertSubscribeLog(l.ctx.Request.Context(), &user.SubscribeLog{ + UserId: userSub.UserId, + UserSubscribeId: userSub.Id, + Token: req.Token, + IP: l.ctx.ClientIP(), + UserAgent: l.ctx.Request.UserAgent(), + }) + if err != nil { + l.Errorw("[Generate Subscribe]insert subscribe log error: %v", logger.Field("error", err.Error())) + } +} + +func (l *SubscribeLogic) getServers(userSub *user.Subscribe) ([]*server.Server, error) { + if l.isSubscriptionExpired(userSub) { + return l.createExpiredServers(), nil + } + + subDetails, err := l.svc.SubscribeModel.FindOne(l.ctx.Request.Context(), userSub.SubscribeId) + if err != nil { + l.Errorw("[Generate Subscribe]find subscribe details error: %v", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe details error: %v", err.Error()) + } + + serverIds := tool.StringToInt64Slice(subDetails.Server) + groupIds := tool.StringToInt64Slice(subDetails.ServerGroup) + + servers, err := l.svc.ServerModel.FindServerDetailByGroupIdsAndIds(l.ctx.Request.Context(), groupIds, serverIds) + if err != nil { + l.Errorw("[Generate Subscribe]find server details error: %v", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find server details error: %v", err.Error()) + } + + return servers, nil +} + +func (l *SubscribeLogic) isSubscriptionExpired(userSub *user.Subscribe) bool { + return userSub.ExpireTime.Unix() < time.Now().Unix() && userSub.ExpireTime.Unix() != 0 +} + +func (l *SubscribeLogic) createExpiredServers() []*server.Server { + enable := true + host := l.getFirstHostLine() + + return []*server.Server{ + { + Name: "Subscribe Expired", + ServerAddr: "127.0.0.1", + RelayMode: "none", + Protocol: "shadowsocks", + Config: "{\"method\":\"aes-256-gcm\",\"port\":1}", + Enable: &enable, + Sort: 0, + }, + { + Name: host, + ServerAddr: "127.0.0.1", + RelayMode: "none", + Protocol: "shadowsocks", + Config: "{\"method\":\"aes-256-gcm\",\"port\":1}", + Enable: &enable, + Sort: 0, + }, + } +} + +func (l *SubscribeLogic) getFirstHostLine() string { + host := l.svc.Config.Host + lines := strings.Split(host, "\n") + if len(lines) > 0 { + return lines[0] + } + return host +} + +func (l *SubscribeLogic) getRules() ([]*server.RuleGroup, error) { + rules, err := l.svc.ServerModel.QueryAllRuleGroup(l.ctx) + if err != nil { + l.Errorw("[Generate Subscribe]find rule group error: %v", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find rule group error: %v", err.Error()) + } + return rules, nil +} + +func (l *SubscribeLogic) buildClientConfig(req *types.SubscribeRequest, userSub *user.Subscribe, servers []*server.Server, rules []*server.RuleGroup) ([]byte, string, error) { + proxyManager := adapter.NewAdapter(servers, rules) + clientType := l.getClientType(req) + var resp []byte + var err error + + l.Logger.Info(fmt.Sprintf("[Generate Subscribe] %s", clientType), logger.Field("ua", req.UA), logger.Field("flag", req.Flag)) + + switch clientType { + case "clash": + resp, err = proxyManager.BuildClash(userSub.UUID) + if err != nil { + l.Errorw("[Generate Subscribe] build clash error", logger.Field("error", err.Error())) + return nil, "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "build clash error: %v", err.Error()) + } + l.setClashHeaders() + case "sing-box": + resp, err = proxyManager.BuildSingbox(userSub.UUID) + if err != nil { + l.Errorw("[Generate Subscribe] build sing-box error", logger.Field("error", err.Error())) + return nil, "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "build sing-box error: %v", err.Error()) + } + case "quantumult": + resp = []byte(proxyManager.BuildQuantumultX(userSub.UUID)) + case "shadowrocket": + resp = proxyManager.BuildShadowrocket(userSub.UUID, shadowrocket.UserInfo{ + Upload: userSub.Upload, + Download: userSub.Download, + TotalTraffic: userSub.Traffic, + ExpiredDate: userSub.ExpireTime, + }) + case "loon": + resp = proxyManager.BuildLoon(userSub.UUID) + case "surfboard": + subsURL := l.getSubscribeURL(userSub.Token) + resp = proxyManager.BuildSurfboard(l.svc.Config.Site.SiteName, surfboard.UserInfo{ + Upload: userSub.Upload, + Download: userSub.Download, + TotalTraffic: userSub.Traffic, + ExpiredDate: userSub.ExpireTime, + UUID: userSub.UUID, + SubscribeURL: subsURL, + }) + l.setSurfboardHeaders() + default: + resp = proxyManager.BuildGeneral(userSub.UUID) + } + + headerInfo := fmt.Sprintf("upload=%d;download=%d;total=%d;expire=%d", + userSub.Upload, userSub.Download, userSub.Traffic, userSub.ExpireTime.Unix()) + + return resp, headerInfo, nil +} + +func (l *SubscribeLogic) setClashHeaders() { + l.ctx.Header("content-disposition", fmt.Sprintf("tattachment;filename*=UTF-8''%s.yaml", l.svc.Config.Site.SiteName)) + l.ctx.Header("Profile-Update-Interval", "24") + l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8") +} + +func (l *SubscribeLogic) setSurfboardHeaders() { + l.ctx.Header("content-disposition", fmt.Sprintf("attachment;filename*=UTF-8''%s.conf", l.svc.Config.Site.SiteName)) + l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8") +} + +func (l *SubscribeLogic) getSubscribeURL(token string) string { + if l.svc.Config.Subscribe.PanDomain { + return fmt.Sprintf("https://%s", l.ctx.Request.Host) + } + + if l.svc.Config.Subscribe.SubscribeDomain != "" { + domains := strings.Split(l.svc.Config.Subscribe.SubscribeDomain, "\n") + return fmt.Sprintf("https://%s%s?token=%s&flag=surfboard", domains[0], l.svc.Config.Subscribe.SubscribePath, token) + } + + return fmt.Sprintf("https://%s%s?token=%s&flag=surfboard", l.ctx.Request.Host, l.svc.Config.Subscribe.SubscribePath, token) +} + +func (l *SubscribeLogic) getClientType(req *types.SubscribeRequest) string { + clientTypeMap := map[string]string{ + "clash": "clash", + "meta": "clash", + "sing-box": "sing-box", + "hiddify": "sing-box", + "surge": "surge", + "quantumult": "quantumult", + "shadowrocket": "shadowrocket", + "loon": "loon", + "surfboard": "surfboard", + } + + findClient := func(s string) string { + s = strings.ToLower(strings.TrimSpace(s)) + if s == "" { + return "" + } + + for key, clientType := range clientTypeMap { + if strings.Contains(s, key) { + return clientType + } + } + + return "" + } + + // 优先检查Flag参数 + if typ := findClient(req.Flag); typ != "" { + return typ + } + + // 其次检查UA参数 + return findClient(req.UA) +} diff --git a/internal/logic/telegram/bot.go b/internal/logic/telegram/bot.go new file mode 100644 index 0000000..3dd7bd8 --- /dev/null +++ b/internal/logic/telegram/bot.go @@ -0,0 +1,124 @@ +package telegram + +import ( + "context" + "encoding/json" + "net/http" + "strconv" + "strings" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/model/auth" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +func GetTelegramConfig(ctx context.Context, svcCtx *svc.ServiceContext) (*types.TelegramConfig, error) { + + data, err := svcCtx.AuthModel.FindOneByMethod(ctx, "telegram") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get Telegram config failed: %v", err.Error()) + } + var telegramConfig auth.TelegramAuthConfig + err = json.Unmarshal([]byte(data.Config), &telegramConfig) + if err != nil { + logger.WithContext(ctx).Error("unmarshal telegram config failed", logger.Field("error", err.Error())) + return nil, err + } + + return &types.TelegramConfig{ + TelegramBotToken: telegramConfig.BotToken, + TelegramNotify: *data.Enabled, + TelegramWebHookDomain: telegramConfig.WebHookDomain, + }, nil +} + +func ApiLink(ctx *gin.Context, svcCtx *svc.ServiceContext, method string) string { + cfg, _ := GetTelegramConfig(ctx, svcCtx) + return "https://api.telegram.org/bot" + cfg.TelegramBotToken + "/" + method +} + +func SendUserMessage(ctx *gin.Context, svcCtx *svc.ServiceContext, u user.User, text string, parseMode string) { + req, _ := http.NewRequest("GET", ApiLink(ctx, svcCtx, "sendMessage"), nil) + q := req.URL.Query() + + userTelegramChatId, ok := findTelegram(&u) + if !ok { + return + } + q.Add("chat_id", strconv.FormatInt(userTelegramChatId, 10)) + if parseMode == "markdown" { + text = strings.ReplaceAll(text, "_", "\\_") + } + q.Add("text", text) + q.Add("parse_mode", parseMode) + req.URL.RawQuery = q.Encode() + _, _ = http.DefaultClient.Do(req) + +} + +func SendAdminMessage(ctx *gin.Context, svcCtx *svc.ServiceContext, text string, parseMode string) { + var adminTelegram []int64 + f := false + adminTelegramJson, err := svcCtx.Redis.Get(ctx, "adminTelegram").Result() + if err == nil { + err = json.Unmarshal([]byte(adminTelegramJson), &adminTelegram) + if err == nil { + f = true + } + } + if !f { + svcCtx.DB.Model(&user.User{}).Where("is_admin = true").Pluck("telegram", &adminTelegram) + val, _ := json.Marshal(adminTelegram) + _ = svcCtx.Redis.Set(ctx, "TelegramConfig", string(val), time.Duration(3600)*time.Second).Err() + } + req, _ := http.NewRequest("GET", ApiLink(ctx, svcCtx, "sendMessage"), nil) + q := req.URL.Query() + if parseMode == "markdown" { + text = strings.ReplaceAll(text, "_", "\\_") + } + q.Add("text", text) + q.Add("parse_mode", parseMode) + for _, telegram := range adminTelegram { + q.Add("chat_id", strconv.FormatInt(telegram, 10)) + req.URL.RawQuery = q.Encode() + _, _ = http.DefaultClient.Do(req) + } +} + +func SetWebhook(ctx *gin.Context, svcCtx *svc.ServiceContext) error { + configs, _ := svcCtx.SystemModel.GetSiteConfig(ctx) + cfg := &types.SiteConfig{} + tool.SystemConfigSliceReflectToStruct(configs, cfg) + req, _ := http.NewRequest("GET", ApiLink(ctx, svcCtx, "setWebhook"), nil) + q := req.URL.Query() + q.Add("url", cfg.Host+"/telegram/webhook") + req.URL.RawQuery = q.Encode() + _, err := http.DefaultClient.Do(req) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set webhook error: %v", err) + } + return nil +} + +func findTelegram(u *user.User) (int64, bool) { + for _, item := range u.AuthMethods { + if item.AuthType == "telegram" { + // string to int64 + parseInt, err := strconv.ParseInt(item.AuthIdentifier, 10, 64) + if err != nil { + return 0, false + } + return parseInt, true + } + + } + return 0, false +} diff --git a/internal/logic/telegram/telegramLogic.go b/internal/logic/telegram/telegramLogic.go new file mode 100644 index 0000000..0eedc4d --- /dev/null +++ b/internal/logic/telegram/telegramLogic.go @@ -0,0 +1,134 @@ +package telegram + +import ( + "context" + "fmt" + "strconv" + "time" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +type TelegramLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewTelegramLogic(ctx context.Context, svcCtx *svc.ServiceContext) *TelegramLogic { + return &TelegramLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *TelegramLogic) TelegramLogic(req *tgbotapi.Update) { + if req.Message != nil && req.Message.Text != "" { + switch req.Message.Command() { + case "traffic": + if err := l.traffic(req.Message.Chat.ID); err != nil { + l.Logger.Error("[TelegramLogic] Traffic Error: ", logger.Field("error", err.Error()), logger.Field("command", req.Message.Command()), logger.Field("chat_id", req.Message.Chat.ID)) + } + case "bind": + if err := l.bind(req.Message.Chat.ID, req.Message.CommandArguments()); err != nil { + l.Logger.Error("[TelegramLogic] Bind Error: ", logger.Field("error", err.Error()), logger.Field("command", req.Message.Command()), logger.Field("chat_id", req.Message.Chat.ID)) + } + case "start": + if err := l.start(req); err != nil { + l.Logger.Error("[TelegramLogic] Start Error: ", logger.Field("error", err.Error()), logger.Field("command", req.Message.Command()), logger.Field("chat_id", req.Message.Chat.ID), logger.Field("text", req.Message.Text)) + } + } + } else { + l.Logger.Error("[TelegramLogic] Message is empty") + } +} + +func (l *TelegramLogic) sendMessage(bot *tgbotapi.BotAPI, message string, userId int64) error { + msg := tgbotapi.NewMessage(userId, message) + msg.ParseMode = "Markdown" + _, err := bot.Send(msg) + return err +} + +func (l *TelegramLogic) traffic(userId int64) error { + return nil +} + +func (l *TelegramLogic) bind(userId int64, token string) error { + return nil +} + +func (l *TelegramLogic) start(req *tgbotapi.Update) error { + if req.Message.CommandArguments() == "" { + return l.sendMessage(l.svcCtx.TelegramBot, "Please bind account!", req.Message.Chat.ID) + } else { + sessionId := req.Message.CommandArguments() + // get session id from redis + sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) + value, err := l.svcCtx.Redis.Get(context.Background(), sessionIdCacheKey).Result() + if err != nil && !errors.Is(err, redis.Nil) { + l.Errorw("TelegramLogic start Redis Get Error: ", logger.Field("error", err.Error()), logger.Field("session", sessionId)) + return l.sendMessage(l.svcCtx.TelegramBot, "Bind failed!", req.Message.Chat.ID) + } + if value == "" { + l.Errorw("TelegramLogic start Redis Get Error: ", logger.Field("error", "session not found"), logger.Field("session", sessionId)) + return l.sendMessage(l.svcCtx.TelegramBot, "Bind failed!", req.Message.Chat.ID) + } + userId, err := strconv.ParseInt(value, 10, 64) + if err != nil { + l.Errorw("TelegramLogic start ParseInt Error: ", logger.Field("error", err.Error()), logger.Field("session", sessionId)) + return l.sendMessage(l.svcCtx.TelegramBot, "Bind failed!", req.Message.Chat.ID) + } + + method, err := l.svcCtx.UserModel.FindUserAuthMethodByPlatform(l.ctx, userId, "telegram") + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorw("TelegramLogic start FindUserAuthMethodByPlatform Error: ", logger.Field("error", err.Error()), logger.Field("userId", userId)) + return l.sendMessage(l.svcCtx.TelegramBot, "Bind failed!", req.Message.Chat.ID) + } + if errors.Is(err, gorm.ErrRecordNotFound) { + if err := l.svcCtx.UserModel.InsertUserAuthMethods(l.ctx, &user.AuthMethods{ + UserId: userId, + AuthType: "telegram", + AuthIdentifier: strconv.FormatInt(req.Message.Chat.ID, 10), + Verified: true, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }); err != nil { + l.Errorw("TelegramLogic start InsertUserAuthMethod Error: ", logger.Field("error", err.Error()), logger.Field("userId", userId)) + return l.sendMessage(l.svcCtx.TelegramBot, "Bind failed!", req.Message.Chat.ID) + } + } else { + method.AuthIdentifier = strconv.FormatInt(req.Message.Chat.ID, 10) + if err := l.svcCtx.UserModel.InsertUserAuthMethods(l.ctx, method); err != nil { + l.Errorw("TelegramLogic start UpdateUserAuthMethod Error: ", logger.Field("error", err.Error()), logger.Field("userId", userId)) + return l.sendMessage(l.svcCtx.TelegramBot, "Bind failed!", req.Message.Chat.ID) + } + } + // update user info to redis + err = l.svcCtx.UserModel.UpdateUserCache(l.ctx, &user.User{ + Id: userId, + }) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "update user cache failed") + } + + text, err := tool.RenderTemplateToString(BindNotify, map[string]string{ + "Id": strconv.FormatInt(userId, 10), + "Time": time.Now().Format("2006-01-02 15:04:05"), + }) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "render template failed") + } + return l.sendMessage(l.svcCtx.TelegramBot, text, req.Message.Chat.ID) + } +} diff --git a/internal/logic/telegram/template.go b/internal/logic/telegram/template.go new file mode 100644 index 0000000..840ceff --- /dev/null +++ b/internal/logic/telegram/template.go @@ -0,0 +1,117 @@ +package telegram + +const BindNotify = `🤖 **尊敬的用户,您已成功绑定 Bot!** + +**绑定账号**: {{.Id}} +**绑定时间**: {{.Time}} + +感谢您的支持!🎉 +您现在可以通过该 Bot 随时管理您的账户和服务。如有任何问题,请联系客服。💬 +` + +const PurchaseNotify = `🎉 **尊敬的用户,您已成功购买服务!** + +**订单编号**: {{.OrderNo}} +**订阅名称**: {{.SubscribeName}} +**订单金额**: **{{.OrderAmount}}** +**到期时间**: {{.ExpireTime}} + +感谢您的支持!💖 +您的服务已成功激活,随时为您提供高速、稳定、安全的网络体验。 +如有疑问,请联系客服,我们将竭诚为您服务!💬` + +const RenewalNotify = `🎉 **尊敬的用户,您已成功续费服务!** + +**订单编号**: {{.OrderNo}} +**订阅名称**: {{.SubscribeName}} +**订单金额**: **{{.OrderAmount}}** +**到期时间**: {{.ExpireTime}} + +感谢您的支持!💖 +您的服务已成功激活,随时为您提供高速、稳定、安全的网络体验。 +如有疑问,请联系客服,我们将竭诚为您服务!💬` + +// RechargeNotify 充值通知 +const RechargeNotify = `💳 **尊敬的用户,您的账户充值已完成!** + +💰 **充值金额**: {{.OrderAmount}} +🏦 **充值方式**: _{{.PaymentMethod}}_ +⏰ **充值时间**: {{.Time}} +📊 **当前账户余额**: **{{.Balance}}** + +感谢您的支持!🎉 +余额可用于购买套餐或其他服务。 +如有疑问,请联系客服,我们将竭诚为您服务!💬` + +// AdminOrderNotify 管理员订单通知 +const AdminOrderNotify = ` +📦 **订单通知** + +🆔 **系统订单号**: {{.OrderNo}} +🔖 **商户订单号**: {{.TradeNo}} +👤 **用户账号**: {{.UserEmail}} +💰 **订单金额**: **{{.OrderAmount}}** +📋 **订单状态**: **{{.OrderStatus}}** +📦 **订阅名称**: _{{.SubscribeName}}_ +⏰ **下单时间**: {{.OrderTime}} +💳 **支付方式**: _{{.PaymentMethod}}_ +` + +// AdminOrderDaily 管理员每日订单统计 +const AdminOrderDaily = ` +📊 **每日流水统计** + +**统计日期**: {{.Date}} +**总订单数**: **{{.Orders}}** +**总成交金额**: **{{.Amount}}** + +**按套餐分类:** +{{.Subscribe}} + +**按支付方式:** +{{.Payment}} + +**总览:** + **当日退款数**: {{.RefundOrders}} 单,退款金额:**{{RefundAmount}}** + **实际入账金额**: **{{ActualAmount}}** + +**请注意**: +以上数据为系统自动统计,仅供参考。如需详细数据或对账,请查看管理后台。 +` + +// SubscribeExpireNotify 订阅到期通知 +const SubscribeExpireNotify = `尊敬的用户,您的订阅即将到期。 + +📦 **订阅名称**: _{{.SubscribeName}}_ +⏰ **到期时间**: {{.ExpiredAt}} +💰 **续费金额**: **{{.RenewalAmount}}** + +为确保服务不受影响,请尽快续费。 +如有疑问,请联系客服,我们将竭诚为您服务!💬` + +// UnbindNotify 解绑通知 +const UnbindNotify = `🤖 尊敬的用户,您好! + +您的账户已成功解绑: + +**用户ID**:{{.Id}} +**解绑时间**:{{.Time}} + +解绑后,您将无法通过该Bot进行账户相关操作。 +如需重新绑定,请访问[绑定页面](#)完成操作。 + +如有任何疑问,请随时联系客服,我们将竭诚为您服务! +感谢您的理解与支持!` + +// ResetTrafficNotify 重置流量通知 +const ResetTrafficNotify = `📊 尊敬的用户,您好! + +您的账户流量已成功重置: + +**用户邮箱**:{{.Email}} +**套餐名称**:{{.SubscribeName}} +**重置时间**:{{.ResetTime}} +**到期时间**:{{.ExpireTime}} + +新的流量额度已生效,感谢您的支持! +如有任何问题,请随时联系客服,我们将竭诚为您服务!💬` diff --git a/internal/middleware/appMiddleware.go b/internal/middleware/appMiddleware.go new file mode 100644 index 0000000..c1bef7e --- /dev/null +++ b/internal/middleware/appMiddleware.go @@ -0,0 +1,282 @@ +package middleware + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/svc" + pkgaes "github.com/perfect-panel/ppanel-server/pkg/aes" +) + +const ( + noWritten = -1 + defaultStatus = http.StatusOK + key = "123456" +) + +func AppMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + if !strings.Contains(c.Request.URL.Path, "/v1/app") { + c.Next() + return + } + rw := NewResponseWriter(c, svc) + if !rw.Decrypt() { + result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidCiphertext), "Invalid ciphertext")) + c.Abort() + return + } + c.Writer = rw + c.Next() + rw.FlushAbort() + } +} + +func NewResponseWriter(c *gin.Context, srvCtx *svc.ServiceContext) (rw *ResponseWriter) { + rw = &ResponseWriter{ + c: c, + body: new(bytes.Buffer), + ResponseWriter: c.Writer, + } + applicationConfig, err := srvCtx.ApplicationModel.FindOneConfig(c, 1) + if err != nil { + logger.Errorf("[AppMiddleware] find application config error: %v", err.Error()) + return + } + if strings.ToUpper(applicationConfig.EncryptionMethod) == "AES" && applicationConfig.EncryptionKey != "" { + rw.encryptionKey = applicationConfig.EncryptionKey + rw.encryptionMethod = applicationConfig.EncryptionMethod + rw.encryption = true + } + return +} + +func (rw *ResponseWriter) Encrypt() { + if !rw.encryption { + return + } + buf := rw.body.Bytes() + params := map[string]interface{}{} + err := json.Unmarshal(buf, ¶ms) + if err != nil { + return + } + data := params["data"] + if data != nil { + var jsonData []byte + str, ok := data.(string) + if ok { + jsonData = []byte(str) + } else { + jsonData, _ = json.Marshal(data) + } + encrypt, iv, err := pkgaes.Encrypt(jsonData, rw.encryptionKey) + if err != nil { + return + } + params["data"] = map[string]interface{}{ + "data": encrypt, + "time": iv, + } + + } + marshal, _ := json.Marshal(params) + rw.body.Reset() + rw.body.Write(marshal) +} + +func (rw *ResponseWriter) Decrypt() bool { + if !rw.encryption { + return true + } + + //判断url链接中是否存在data和iv数据,存在就进行解密并设置回去 + query := rw.c.Request.URL.Query() + dataStr := query.Get("data") + timeStr := query.Get("time") + if dataStr != "" && timeStr != "" { + decrypt, err := pkgaes.Decrypt(dataStr, rw.encryptionKey, timeStr) + if err == nil { + params := map[string]interface{}{} + err = json.Unmarshal([]byte(decrypt), ¶ms) + if err == nil { + for k, v := range params { + query.Set(k, fmt.Sprintf("%v", v)) + } + query.Del("data") + query.Del("time") + rw.c.Request.RequestURI = fmt.Sprintf("%s?%s", rw.c.Request.RequestURI[:strings.Index(rw.c.Request.RequestURI, "?")], query.Encode()) + rw.c.Request.URL.RawQuery = query.Encode() + } + } + } + + //判断body是否存在数据,存在就尝试解密,并设置回去 + body, err := io.ReadAll(rw.c.Request.Body) + if err != nil { + return true + } + + if len(body) == 0 { + return true + } + + params := map[string]interface{}{} + err = json.Unmarshal(body, ¶ms) + data := params["data"] + nonce := params["time"] + if err != nil || data == nil { + return false + } + + str, ok := data.(string) + if !ok { + return false + } + iv, ok := nonce.(string) + if !ok { + return false + } + + decrypt, err := pkgaes.Decrypt(str, rw.encryptionKey, iv) + if err != nil { + return false + } + rw.c.Request.Body = io.NopCloser(bytes.NewBuffer([]byte(decrypt))) + return true +} + +func (rw *ResponseWriter) FlushAbort() { + defer rw.c.Abort() + responseBody := rw.body.String() + fmt.Println("Original Response Body:", responseBody) + rw.flush = true + if rw.encryption { + rw.Encrypt() + } + _, err := rw.Write(rw.body.Bytes()) + if err != nil { + return + } +} + +type ResponseWriter struct { + http.ResponseWriter + size int + status int + flush bool + body *bytes.Buffer + c *gin.Context + encryption bool + encryptionKey string + encryptionMethod string +} + +func (rw *ResponseWriter) Unwrap() http.ResponseWriter { + return rw.ResponseWriter +} + +//nolint:unused +func (rw *ResponseWriter) reset(writer http.ResponseWriter) { + rw.ResponseWriter = writer + rw.size = noWritten + rw.status = defaultStatus +} + +func (rw *ResponseWriter) WriteHeader(code int) { + if code > 0 && rw.status != code { + if rw.Written() { + return + } + rw.status = code + } +} + +func (rw *ResponseWriter) WriteHeaderNow() { + if !rw.Written() { + rw.size = 0 + rw.ResponseWriter.WriteHeader(rw.status) + } +} + +func (rw *ResponseWriter) Write(data []byte) (n int, err error) { + if rw.flush { + rw.WriteHeaderNow() + n, err = rw.ResponseWriter.Write(data) + rw.size += n + } else { + rw.body.Write(data) + } + return +} + +func (rw *ResponseWriter) WriteString(s string) (n int, err error) { + if rw.flush { + rw.WriteHeaderNow() + n, err = rw.ResponseWriter.Write([]byte(s)) + rw.size += n + } else { + rw.body.Write([]byte(s)) + } + return +} + +func (rw *ResponseWriter) Status() int { + return rw.status +} + +func (rw *ResponseWriter) Size() int { + return rw.size +} + +func (rw *ResponseWriter) Written() bool { + return rw.size != noWritten +} + +// Hijack implements the http.Hijacker interface. +func (rw *ResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + if rw.size < 0 { + rw.size = 0 + } + return rw.ResponseWriter.(http.Hijacker).Hijack() +} + +// CloseNotify implements the http.CloseNotifier interface. +func (rw *ResponseWriter) CloseNotify() <-chan bool { + // 通过 r.Context().Done() 来监听请求的取消 + done := rw.c.Request.Context().Done() + closed := make(chan bool) + + // 当上下文被取消时,通过 closed channel 发送通知 + go func() { + <-done + closed <- true + }() + + return closed +} + +// Flush implements the http.Flusher interface. +func (rw *ResponseWriter) Flush() { + rw.WriteHeaderNow() + rw.ResponseWriter.(http.Flusher).Flush() +} + +func (rw *ResponseWriter) Pusher() (pusher http.Pusher) { + if pusher, ok := rw.ResponseWriter.(http.Pusher); ok { + return pusher + } + return nil +} diff --git a/internal/middleware/authMiddleware.go b/internal/middleware/authMiddleware.go new file mode 100644 index 0000000..8bf45a8 --- /dev/null +++ b/internal/middleware/authMiddleware.go @@ -0,0 +1,85 @@ +package middleware + +import ( + "context" + "fmt" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/jwt" + "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" +) + +func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + ctx := c.Request.Context() + + jwtConfig := svc.Config.JwtAuth + // get token from header + token := c.GetHeader("Authorization") + if token == "" { + logger.WithContext(c.Request.Context()).Debug("[AuthMiddleware] Token Empty") + result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.ErrorTokenEmpty), "Token Empty")) + c.Abort() + return + } + // parse token + claims, err := jwt.ParseJwtToken(token, jwtConfig.AccessSecret) + if err != nil { + logger.WithContext(c.Request.Context()).Debug("[AuthMiddleware] ParseJwtToken", logger.Field("error", err.Error()), logger.Field("token", token)) + result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.ErrorTokenExpire), "Token Invalid")) + c.Abort() + return + } + // get user id from token + userId := int64(claims["UserId"].(float64)) + // get session id from token + sessionId := claims["SessionId"].(string) + // get session id from redis + sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) + value, err := svc.Redis.Get(c, sessionIdCacheKey).Result() + if err != nil { + logger.WithContext(c.Request.Context()).Debug("[AuthMiddleware] Redis Get", logger.Field("error", err.Error()), logger.Field("sessionId", sessionId)) + result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")) + c.Abort() + return + } + + //verify user id + if value != fmt.Sprintf("%v", userId) { + logger.WithContext(c.Request.Context()).Debug("[AuthMiddleware] Invalid Access", logger.Field("userId", userId), logger.Field("sessionId", sessionId)) + result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")) + c.Abort() + return + } + + userInfo, err := svc.UserModel.FindOne(c, userId) + if err != nil { + logger.WithContext(c.Request.Context()).Debug("[AuthMiddleware] UserModel FindOne", logger.Field("error", err.Error()), logger.Field("userId", userId)) + result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Database Query Error")) + c.Abort() + return + } + // admin verify + paths := strings.Split(c.Request.URL.Path, "/") + if tool.StringSliceContains(paths, "admin") && !*userInfo.IsAdmin { + logger.WithContext(c.Request.Context()).Debug("[AuthMiddleware] Not Admin User", logger.Field("userId", userId), logger.Field("sessionId", sessionId)) + result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")) + c.Abort() + return + } + ctx = context.WithValue(ctx, constant.CtxKeyUser, userInfo) + ctx = context.WithValue(ctx, constant.CtxKeySessionID, sessionId) + c.Request = c.Request.WithContext(ctx) + c.Next() + } +} diff --git a/internal/middleware/corsMiddleware.go b/internal/middleware/corsMiddleware.go new file mode 100644 index 0000000..e6c94df --- /dev/null +++ b/internal/middleware/corsMiddleware.go @@ -0,0 +1,28 @@ +package middleware + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +func CorsMiddleware(c *gin.Context) { + origin := c.Request.Header.Get("Origin") + if origin != "" { + c.Writer.Header().Set("Access-Control-Allow-Origin", origin) + } else { + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + } + // c.Writer.Header().Set("Access-Control-Allow-Origin", c.Request.Host) + c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Origin, X-CSRF-Token, Authorization, AccessToken, Token, Range") + c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers") + c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + c.Writer.Header().Set("Access-Control-Max-Age", "172800") + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(http.StatusNoContent) + return + } + + c.Next() +} diff --git a/internal/middleware/loggerMiddleware.go b/internal/middleware/loggerMiddleware.go new file mode 100644 index 0000000..a7faaa7 --- /dev/null +++ b/internal/middleware/loggerMiddleware.go @@ -0,0 +1,110 @@ +package middleware + +import ( + "bytes" + "encoding/json" + "io" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/svc" +) + +type responseBodyWriter struct { + gin.ResponseWriter + body *bytes.Buffer +} + +func (r responseBodyWriter) Write(b []byte) (int, error) { + r.body.Write(b) + return r.ResponseWriter.Write(b) +} + +func LoggerMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + // get response body + w := &responseBodyWriter{body: &bytes.Buffer{}, ResponseWriter: c.Writer} + c.Writer = w + // get request body + var requestBody []byte + if c.Request.Body != nil { + // c.Request.Body It can only be read once, and after reading, it needs to be reassigned to c.Request Body + requestBody, _ = io.ReadAll(c.Request.Body) + // After reading, reassign c.Request Body , For subsequent operations + c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody)) + } + // start time + start := time.Now() + c.Next() + // Start recording logs + cost := time.Since(start) + responseStatus := c.Writer.Status() + logs := []logger.LogField{ + { + Key: "status", + Value: responseStatus, + }, + { + Key: "request", + Value: c.Request.Method + " " + c.Request.URL.String(), + }, + { + Key: "query", + Value: c.Request.URL.RawQuery, + }, + { + Key: "ip", + Value: c.ClientIP(), + }, + { + Key: "user-agent", + Value: c.Request.UserAgent(), + }, + } + if c.Errors.Last() != nil { + var e *xerr.CodeError + var errMessage string + if errors.As(c.Errors.Last().Err, &e) { + errMessage = e.GetErrMsg() + } else { + errMessage = c.Errors.Last().Error() + } + logs = append(logs, logger.Field("error", errMessage)) + } + if c.Request.Method == "POST" || c.Request.Method == "PUT" || c.Request.Method == "DELETE" { + // request content + logs = append(logs, logger.Field("request_body", string(maskSensitiveFields(requestBody, []string{"password", "old_password", "new_password"})))) + // response content + logs = append(logs, logger.Field("response_body", w.body.String())) + } + logs = append(logs, logger.Field("duration", cost)) + if responseStatus >= 500 && responseStatus <= 599 { + logger.WithContext(c.Request.Context()).Errorw("HTTP Error", logs...) + } else { + logger.WithContext(c.Request.Context()).Infow("HTTP Request", logs...) + } + } +} + +func maskSensitiveFields(data []byte, fieldsToMask []string) []byte { + var jsonData map[string]interface{} + if err := json.Unmarshal(data, &jsonData); err != nil { + return data + } + + for _, field := range fieldsToMask { + if _, exists := jsonData[field]; exists { + jsonData[field] = "***" // use *** to mask sensitive fields + } + } + maskedData, err := json.Marshal(jsonData) + if err != nil { + return data + } + return maskedData +} diff --git a/internal/middleware/notifyMiddleware.go b/internal/middleware/notifyMiddleware.go new file mode 100644 index 0000000..ba9dc32 --- /dev/null +++ b/internal/middleware/notifyMiddleware.go @@ -0,0 +1,38 @@ +package middleware + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/svc" +) + +type PaymentParams struct { + Platform string `uri:"platform"` + Token string `uri:"token"` +} + +func NotifyMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + ctx := c.Request.Context() + var params PaymentParams + // Get platform and token from uri + if err := c.ShouldBindUri(¶ms); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + c.Abort() + return + } + config, err := svc.PaymentModel.FindOneByPaymentToken(ctx, params.Token) + if err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + c.Abort() + return + } + ctx = context.WithValue(ctx, constant.CtxKeyPlatform, config.Platform) + ctx = context.WithValue(ctx, constant.CtxKeyPayment, config) + c.Request = c.Request.WithContext(ctx) + c.Next() + } +} diff --git a/internal/middleware/panDomainMiddleware.go b/internal/middleware/panDomainMiddleware.go new file mode 100644 index 0000000..3e51bee --- /dev/null +++ b/internal/middleware/panDomainMiddleware.go @@ -0,0 +1,35 @@ +package middleware + +import ( + "strings" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/logic/subscribe" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" +) + +func PanDomainMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + if svc.Config.Subscribe.PanDomain { + domain := c.Request.Host + domainArr := strings.Split(domain, ".") + domainFirst := domainArr[0] + request := types.SubscribeRequest{ + Token: domainFirst, + Flag: domainArr[1], + UA: c.Request.Header.Get("User-Agent"), + } + l := subscribe.NewSubscribeLogic(c, svc) + resp, err := l.Generate(&request) + if err != nil { + return + } + c.Header("subscription-userinfo", resp.Header) + c.String(200, "%s", string(resp.Config)) + c.Abort() + return + } + c.Next() + } +} diff --git a/internal/middleware/serverMiddleware.go b/internal/middleware/serverMiddleware.go new file mode 100644 index 0000000..0c7c221 --- /dev/null +++ b/internal/middleware/serverMiddleware.go @@ -0,0 +1,19 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/internal/svc" +) + +func ServerMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + if key, ok := c.GetQuery("secret_key"); ok { + if key == svc.Config.Node.NodeSecret { + c.Next() + return + } + } + c.String(403, "Forbidden") + c.Abort() + } +} diff --git a/internal/middleware/traceMiddleware.go b/internal/middleware/traceMiddleware.go new file mode 100644 index 0000000..0793041 --- /dev/null +++ b/internal/middleware/traceMiddleware.go @@ -0,0 +1,111 @@ +package middleware + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.24.0" + oteltrace "go.opentelemetry.io/otel/trace" + + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/trace" +) + +// statusByWriter returns a span status code and message for an HTTP status code +// value returned by a server. Status codes in the 400-499 range are not +// returned as errors. +func statusByWriter(code int) (codes.Code, string) { + if code < 100 || code >= 600 { + return codes.Error, fmt.Sprintf("Invalid HTTP status code %d", code) + } + if code >= 500 { + return codes.Error, "" + } + return codes.Unset, "" +} + +func requestAttributes(req *http.Request) []attribute.KeyValue { + protoN := strings.SplitN(req.Proto, "/", 2) + remoteAddrN := strings.SplitN(req.RemoteAddr, ":", 2) + + return []attribute.KeyValue{ + semconv.HTTPRequestMethodKey.String(req.Method), + semconv.HTTPUserAgentKey.String(req.UserAgent()), + semconv.HTTPRequestContentLengthKey.Int64(req.ContentLength), + + semconv.URLFullKey.String(req.URL.String()), + semconv.URLSchemeKey.String(req.URL.Scheme), + semconv.URLFragmentKey.String(req.URL.Fragment), + semconv.URLPathKey.String(req.URL.Path), + semconv.URLQueryKey.String(req.URL.RawQuery), + + semconv.NetworkProtocolNameKey.String(strings.ToLower(protoN[0])), + semconv.NetworkProtocolVersionKey.String(protoN[1]), + + semconv.ClientAddressKey.String(remoteAddrN[0]), + semconv.ClientPortKey.String(remoteAddrN[1]), + } +} + +func TraceMiddleware(_ *svc.ServiceContext) func(ctx *gin.Context) { + return func(c *gin.Context) { + ctx := c.Request.Context() + tracer := trace.TracerFromContext(ctx) + + spanName := c.FullPath() + method := c.Request.Method + + ctx, span := tracer.Start( + ctx, + fmt.Sprintf("%s %s", method, spanName), + oteltrace.WithSpanKind(oteltrace.SpanKindServer), + ) + defer span.End() + + requestId, err := uuid.NewV7() + if err != nil { + logger.Errorw( + "failed to generate request id in uuid v7 format, fallback to uuid v4", + logger.Field("error", err), + ) + requestId = uuid.New() + } + c.Header(trace.RequestIdKey, requestId.String()) + + span.SetAttributes(requestAttributes(c.Request)...) + span.SetAttributes( + attribute.String("http.request_id", requestId.String()), + semconv.HTTPRouteKey.String(c.FullPath()), + ) + // context with request host + ctx = context.WithValue(ctx, constant.CtxKeyRequestHost, c.Request.Host) + // restructure context + c.Request = c.Request.WithContext(ctx) + + c.Next() + + // handle response related attributes + status := c.Writer.Status() + span.SetStatus(statusByWriter(status)) + if status > 0 { + span.SetAttributes(semconv.HTTPResponseStatusCodeKey.Int(status)) + } + if len(c.Errors) > 0 { + span.SetStatus(codes.Error, c.Errors.String()) + for _, err := range c.Errors { + span.RecordError(err.Err) + } + } + + span.SetAttributes(semconv.HTTPResponseBodySizeKey.Int(c.Writer.Size())) + } +} diff --git a/internal/model/ads/ads.go b/internal/model/ads/ads.go new file mode 100644 index 0000000..7390fff --- /dev/null +++ b/internal/model/ads/ads.go @@ -0,0 +1,21 @@ +package ads + +import "time" + +type Ads struct { + Id int64 `gorm:"primaryKey"` + Title string `gorm:"type:varchar(255);default:'';not null;comment:Ads title"` + Type string `gorm:"type:varchar(255);default:'';not null;comment:Ads type"` + Content string `gorm:"type:text;comment:Ads content"` + Description string `gorm:"type:text;comment:Ads descriptor"` + TargetURL string `gorm:"type:varchar(512);default:'';comment:Ads target url"` + StartTime time.Time `gorm:"type:datetime;comment:Ads start time"` + EndTime time.Time `gorm:"type:datetime;comment:Ads end time"` + Status int `gorm:"type:TINYINT;default:0;comment:Ads status,0 disable,1 enable"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (Ads) TableName() string { + return "ads" +} diff --git a/internal/model/ads/default.go b/internal/model/ads/default.go new file mode 100644 index 0000000..52f9e99 --- /dev/null +++ b/internal/model/ads/default.go @@ -0,0 +1,112 @@ +package ads + +import ( + "context" + "errors" + "fmt" + + "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var _ Model = (*customAdsModel)(nil) +var ( + cacheAdsIdPrefix = "cache:ads:id:" +) + +type ( + Model interface { + adsModel + customAdsLogicModel + } + adsModel interface { + Insert(ctx context.Context, data *Ads) error + FindOne(ctx context.Context, id int64) (*Ads, error) + Update(ctx context.Context, data *Ads) error + Delete(ctx context.Context, id int64) error + Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + } + + customAdsModel struct { + *defaultAdsModel + } + defaultAdsModel struct { + cache.CachedConn + table string + } +) + +func newAdsModel(db *gorm.DB, c *redis.Client) *defaultAdsModel { + return &defaultAdsModel{ + CachedConn: cache.NewConn(db, c), + table: "`ads`", + } +} + +//nolint:unused +func (m *defaultAdsModel) batchGetCacheKeys(ads ...*Ads) []string { + var keys []string + for _, ad := range ads { + keys = append(keys, m.getCacheKeys(ad)...) + } + return keys + +} +func (m *defaultAdsModel) getCacheKeys(data *Ads) []string { + if data == nil { + return []string{} + } + adsIdKey := fmt.Sprintf("%s%v", cacheAdsIdPrefix, data.Id) + cacheKeys := []string{ + adsIdKey, + } + return cacheKeys +} + +func (m *defaultAdsModel) Insert(ctx context.Context, data *Ads) error { + err := m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Create(&data).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultAdsModel) FindOne(ctx context.Context, id int64) (*Ads, error) { + AdsIdKey := fmt.Sprintf("%s%v", cacheAdsIdPrefix, id) + var resp Ads + err := m.QueryCtx(ctx, &resp, AdsIdKey, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Ads{}).Where("`id` = ?", id).First(&resp).Error + }) + return &resp, err +} + +func (m *defaultAdsModel) Update(ctx context.Context, data *Ads) error { + old, err := m.FindOne(ctx, data.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Save(data).Error + }, m.getCacheKeys(old)...) + return err +} + +func (m *defaultAdsModel) Delete(ctx context.Context, id int64) error { + data, err := m.FindOne(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Delete(&Ads{}, id).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultAdsModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { + return m.TransactCtx(ctx, fn) +} diff --git a/internal/model/ads/model.go b/internal/model/ads/model.go new file mode 100644 index 0000000..4ad53cb --- /dev/null +++ b/internal/model/ads/model.go @@ -0,0 +1,41 @@ +package ads + +import ( + "context" + + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +type customAdsLogicModel interface { + GetAdsListByPage(ctx context.Context, page, size int, filter Filter) (int64, []*Ads, error) +} + +// NewModel returns a model for the database table. +func NewModel(conn *gorm.DB, c *redis.Client) Model { + return &customAdsModel{ + defaultAdsModel: newAdsModel(conn, c), + } +} + +type Filter struct { + Status *int + Search string +} + +// GetAdsListByPage get ads list by page +func (m *customAdsModel) GetAdsListByPage(ctx context.Context, page, size int, filter Filter) (int64, []*Ads, error) { + var list []*Ads + var total int64 + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + conn = conn.Model(&Ads{}) + if filter.Status != nil { + conn = conn.Where("`status` = ?", *filter.Status) + } + if filter.Search != "" { + conn = conn.Where("`title` LIKE ? OR `content` LIKE ?", "%"+filter.Search+"%", "%"+filter.Search+"%") + } + return conn.Count(&total).Offset((page - 1) * size).Limit(size).Find(v).Error + }) + return total, list, err +} diff --git a/internal/model/announcement/announcement.go b/internal/model/announcement/announcement.go new file mode 100644 index 0000000..388314f --- /dev/null +++ b/internal/model/announcement/announcement.go @@ -0,0 +1,18 @@ +package announcement + +import "time" + +type Announcement struct { + Id int64 `gorm:"primaryKey"` + Title string `gorm:"type:varchar(255);not null;default:'';comment:Title"` + Content string `gorm:"type:text;comment:Content"` + Show *bool `gorm:"type:tinyint(1);not null;default:0;comment:Show"` + Pinned *bool `gorm:"type:tinyint(1);not null;default:0;comment:Pinned"` + Popup *bool `gorm:"type:tinyint(1);not null;default:0;comment:Popup"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (Announcement) TableName() string { + return "announcement" +} diff --git a/internal/model/announcement/default.go b/internal/model/announcement/default.go new file mode 100644 index 0000000..36ccae4 --- /dev/null +++ b/internal/model/announcement/default.go @@ -0,0 +1,117 @@ +package announcement + +import ( + "context" + "errors" + "fmt" + + "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var _ Model = (*customAnnouncementModel)(nil) +var ( + cacheAnnouncementIdPrefix = "cache:announcement:id:" +) + +type ( + Model interface { + announcementModel + customAnnouncementLogicModel + } + announcementModel interface { + Insert(ctx context.Context, data *Announcement) error + FindOne(ctx context.Context, id int64) (*Announcement, error) + Update(ctx context.Context, data *Announcement) error + Delete(ctx context.Context, id int64) error + Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + } + + customAnnouncementModel struct { + *defaultAnnouncementModel + } + defaultAnnouncementModel struct { + cache.CachedConn + table string + } +) + +func newAnnouncementModel(db *gorm.DB, c *redis.Client) *defaultAnnouncementModel { + return &defaultAnnouncementModel{ + CachedConn: cache.NewConn(db, c), + table: "`announcement`", + } +} + +//nolint:unused +func (m *defaultAnnouncementModel) batchGetCacheKeys(Announcements ...*Announcement) []string { + var keys []string + for _, announcement := range Announcements { + keys = append(keys, m.getCacheKeys(announcement)...) + } + return keys + +} +func (m *defaultAnnouncementModel) getCacheKeys(data *Announcement) []string { + if data == nil { + return []string{} + } + announcementIdKey := fmt.Sprintf("%s%v", cacheAnnouncementIdPrefix, data.Id) + cacheKeys := []string{ + announcementIdKey, + } + return cacheKeys +} + +func (m *defaultAnnouncementModel) Insert(ctx context.Context, data *Announcement) error { + err := m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Create(&data).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultAnnouncementModel) FindOne(ctx context.Context, id int64) (*Announcement, error) { + AnnouncementIdKey := fmt.Sprintf("%s%v", cacheAnnouncementIdPrefix, id) + var resp Announcement + err := m.QueryCtx(ctx, &resp, AnnouncementIdKey, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Announcement{}).Where("`id` = ?", id).First(&resp).Error + }) + switch { + case err == nil: + return &resp, nil + default: + return nil, err + } +} + +func (m *defaultAnnouncementModel) Update(ctx context.Context, data *Announcement) error { + old, err := m.FindOne(ctx, data.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Save(data).Error + }, m.getCacheKeys(old)...) + return err +} + +func (m *defaultAnnouncementModel) Delete(ctx context.Context, id int64) error { + data, err := m.FindOne(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Delete(&Announcement{}, id).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultAnnouncementModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { + return m.TransactCtx(ctx, fn) +} diff --git a/internal/model/announcement/model.go b/internal/model/announcement/model.go new file mode 100644 index 0000000..f5575a8 --- /dev/null +++ b/internal/model/announcement/model.go @@ -0,0 +1,49 @@ +package announcement + +import ( + "context" + + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +type customAnnouncementLogicModel interface { + GetAnnouncementListByPage(ctx context.Context, page, size int, filter Filter) (int64, []*Announcement, error) +} + +// NewModel returns a model for the database table. +func NewModel(conn *gorm.DB, c *redis.Client) Model { + return &customAnnouncementModel{ + defaultAnnouncementModel: newAnnouncementModel(conn, c), + } +} + +type Filter struct { + Show *bool + Pinned *bool + Popup *bool + Search string +} + +// GetAnnouncementListByPage get announcement list by page +func (m *customAnnouncementModel) GetAnnouncementListByPage(ctx context.Context, page, size int, filter Filter) (int64, []*Announcement, error) { + var list []*Announcement + var total int64 + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + conn = conn.Model(&Announcement{}) + if filter.Show != nil { + conn = conn.Where("`show` = ?", *filter.Show) + } + if filter.Pinned != nil { + conn = conn.Where("`pinned` = ?", *filter.Pinned) + } + if filter.Popup != nil { + conn = conn.Where("`popup` = ?", *filter.Popup) + } + if filter.Search != "" { + conn = conn.Where("`title` LIKE ? OR `content` LIKE ?", "%"+filter.Search+"%", "%"+filter.Search+"%") + } + return conn.Count(&total).Offset((page - 1) * size).Limit(size).Find(v).Error + }) + return total, list, err +} diff --git a/internal/model/application/application.go b/internal/model/application/application.go new file mode 100644 index 0000000..196e9e8 --- /dev/null +++ b/internal/model/application/application.go @@ -0,0 +1,54 @@ +package application + +import ( + "time" +) + +type Application struct { + Id int64 `gorm:"primary_key"` + Name string `gorm:"type:varchar(255);default:'';not null;comment:应用名称"` + Icon string `gorm:"type:text;not null;comment:应用图标"` + Description string `gorm:"type:text;comment:更新描述"` + SubscribeType string `gorm:"type:varchar(50);default:'';not null;comment:订阅类型"` + ApplicationVersions []ApplicationVersion + CreatedAt time.Time `gorm:"<-:create;comment:创建时间"` + UpdatedAt time.Time `gorm:"comment:更新时间"` +} + +func (Application) TableName() string { + return "application" +} + +type ApplicationVersion struct { + Id int64 `gorm:"primary_key"` + Url string `gorm:"type:varchar(255);default:'';not null;comment:应用地址"` + Version string `gorm:"type:varchar(255);default:'';not null;comment:应用版本"` + Platform string `gorm:"type:varchar(50);default:'';not null;comment:应用平台"` + IsDefault bool `gorm:"type:tinyint(1);not null;default:0;comment:默认版本"` + Description string `gorm:"type:text;comment:更新描述"` + ApplicationId int64 `gorm:"comment:所属应用"` + CreatedAt time.Time `gorm:"<-:create;comment:创建时间"` + UpdatedAt time.Time `gorm:"comment:更新时间"` +} + +func (ApplicationVersion) TableName() string { + return "application_version" +} + +type ApplicationConfig struct { + Id int64 `gorm:"primary_key"` + AppId int64 `gorm:"type:int;not null;default:0;comment:App id"` + EncryptionKey string `gorm:"type:text;comment:Encryption Key"` + EncryptionMethod string `gorm:"type:varchar(255);comment:Encryption Method"` + Domains string `gorm:"type:text"` + StartupPicture string `gorm:"type:text"` + StartupPictureSkipTime int64 `gorm:"type:int;not null;default:0;comment:Startup Picture Skip Time"` + InvitationLink string `gorm:"Invitation link"` + KrWebsiteId string `gorm:"type:varchar(255);default:'';comment:Kr Website ID"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (ApplicationConfig) TableName() string { + return "application_config" +} diff --git a/internal/model/application/default.go b/internal/model/application/default.go new file mode 100644 index 0000000..857637d --- /dev/null +++ b/internal/model/application/default.go @@ -0,0 +1,245 @@ +package application + +import ( + "context" + "errors" + "fmt" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var _ Model = (*customApplicationModel)(nil) +var ( + cacheApplicationIdPrefix = "cache:application:id:" + cacheApplicationConfigIdPrefix = "cache:application:config:id:" + cacheApplicationVersionIdPrefix = "cache:application:version:id:" +) + +type ( + Model interface { + applicationModel + customApplicationLogicModel + } + applicationModel interface { + Insert(ctx context.Context, data *Application) error + FindOne(ctx context.Context, id int64) (*Application, error) + Update(ctx context.Context, data *Application) error + Delete(ctx context.Context, id int64) error + InsertVersion(ctx context.Context, data *ApplicationVersion) error + FindOneVersion(ctx context.Context, id int64) (*ApplicationVersion, error) + UpdateVersion(ctx context.Context, data *ApplicationVersion) error + InsertConfig(ctx context.Context, data *ApplicationConfig) error + FindOneConfig(ctx context.Context, id int64) (*ApplicationConfig, error) + UpdateConfig(ctx context.Context, data *ApplicationConfig) error + DeleteVersion(ctx context.Context, id int64) error + Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + } + + customApplicationModel struct { + *defaultApplicationModel + } + defaultApplicationModel struct { + cache.CachedConn + table string + } +) + +func newApplicationModel(db *gorm.DB, c *redis.Client) *defaultApplicationModel { + return &defaultApplicationModel{ + CachedConn: cache.NewConn(db, c), + table: "`Application`", + } +} + +func (m *defaultApplicationModel) getCacheKeys(data *Application) []string { + if data == nil { + return []string{} + } + ApplicationIdKey := fmt.Sprintf("%s%v", cacheApplicationIdPrefix, data.Id) + cacheKeys := []string{ + ApplicationIdKey, + config.ApplicationKey, + } + return cacheKeys +} + +func (m *defaultApplicationModel) Insert(ctx context.Context, data *Application) error { + err := m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Create(&data).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultApplicationModel) FindOne(ctx context.Context, id int64) (*Application, error) { + ApplicationIdKey := fmt.Sprintf("%s%v", cacheApplicationIdPrefix, id) + var resp Application + err := m.QueryCtx(ctx, &resp, ApplicationIdKey, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Application{}).Preload("ApplicationVersions").Where("`id` = ?", id).First(&resp).Error + }) + switch { + case err == nil: + return &resp, nil + default: + return nil, err + } +} + +func (m *defaultApplicationModel) Update(ctx context.Context, data *Application) error { + old, err := m.FindOne(ctx, data.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Save(data).Error + }, m.getCacheKeys(old)...) + return err +} + +func (m *defaultApplicationModel) Delete(ctx context.Context, id int64) error { + data, err := m.FindOne(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + err = db.Where("application_id = ?", id).Delete(&ApplicationVersion{}).Error + if err != nil { + return err + } + return db.Delete(&Application{}, id).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultApplicationModel) getVersionCacheKeys(data *ApplicationVersion) []string { + if data == nil { + return []string{} + } + ApplicationVersionIdKey := fmt.Sprintf("%s%v", cacheApplicationVersionIdPrefix, data.Id) + cacheKeys := []string{ + ApplicationVersionIdKey, + config.ApplicationKey, + } + return cacheKeys +} +func (m *defaultApplicationModel) getConfigCacheKeys(data *ApplicationConfig) []string { + if data == nil { + return []string{} + } + ApplicationConfigIdKey := fmt.Sprintf("%s%v", cacheApplicationConfigIdPrefix, data.Id) + cacheKeys := []string{ + ApplicationConfigIdKey, + config.ApplicationKey, + } + return cacheKeys +} + +func (m *defaultApplicationModel) InsertVersion(ctx context.Context, data *ApplicationVersion) error { + err := m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Transaction(func(tx *gorm.DB) error { + if data.IsDefault { + err := tx.Model(&ApplicationVersion{}). + Where("application_id = ? and platform = ? and default_version = ?", data.ApplicationId, data.Platform, data.IsDefault). + Updates(map[string]interface{}{"default_version": false}).Error + if err != nil { + return err + } + } + return tx.Create(&data).Error + }) + }, m.getVersionCacheKeys(data)...) + return err +} + +func (m *defaultApplicationModel) FindOneVersion(ctx context.Context, id int64) (*ApplicationVersion, error) { + ApplicationVersionIdKey := fmt.Sprintf("%s%v", cacheApplicationVersionIdPrefix, id) + var resp ApplicationVersion + err := m.QueryCtx(ctx, &resp, ApplicationVersionIdKey, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&ApplicationVersion{}).Where("`id` = ?", id).First(&resp).Error + }) + switch { + case err == nil: + return &resp, nil + default: + return nil, err + } +} + +func (m *defaultApplicationModel) UpdateVersion(ctx context.Context, data *ApplicationVersion) error { + old, err := m.FindOneVersion(ctx, data.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Transaction(func(tx *gorm.DB) error { + if data.IsDefault { + err := tx.Model(&ApplicationVersion{}). + Where("application_id = ? and platform = ? and default_version = ?", data.ApplicationId, data.Platform, data.IsDefault). + Updates(map[string]interface{}{"default_version": false}).Error + if err != nil { + return err + } + } + return tx.Save(data).Error + }) + }, m.getVersionCacheKeys(old)...) + return err +} + +func (m *defaultApplicationModel) InsertConfig(ctx context.Context, data *ApplicationConfig) error { + err := m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Create(&data).Error + }, m.getConfigCacheKeys(data)...) + return err +} + +func (m *defaultApplicationModel) FindOneConfig(ctx context.Context, id int64) (*ApplicationConfig, error) { + ApplicationConfigIdKey := fmt.Sprintf("%s%v", cacheApplicationConfigIdPrefix, id) + var resp ApplicationConfig + err := m.QueryCtx(ctx, &resp, ApplicationConfigIdKey, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&ApplicationConfig{}).Where("`id` = ?", id).First(&resp).Error + }) + switch { + case err == nil: + return &resp, nil + default: + return nil, err + } +} + +func (m *defaultApplicationModel) UpdateConfig(ctx context.Context, data *ApplicationConfig) error { + old, err := m.FindOneConfig(ctx, data.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Save(data).Error + }, m.getConfigCacheKeys(old)...) + return err +} + +func (m *defaultApplicationModel) DeleteVersion(ctx context.Context, id int64) error { + data, err := m.FindOneVersion(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Delete(&ApplicationVersion{}, id).Error + }, m.getVersionCacheKeys(data)...) + return err +} + +func (m *defaultApplicationModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { + return m.TransactCtx(ctx, fn) +} diff --git a/internal/model/application/model.go b/internal/model/application/model.go new file mode 100644 index 0000000..3bd5b0a --- /dev/null +++ b/internal/model/application/model.go @@ -0,0 +1,16 @@ +package application + +import ( + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +type customApplicationLogicModel interface { +} + +// NewModel returns a model for the database table. +func NewModel(conn *gorm.DB, c *redis.Client) Model { + return &customApplicationModel{ + defaultApplicationModel: newApplicationModel(conn, c), + } +} diff --git a/internal/model/auth/auth.go b/internal/model/auth/auth.go new file mode 100644 index 0000000..f98d5cd --- /dev/null +++ b/internal/model/auth/auth.go @@ -0,0 +1,272 @@ +package auth + +import ( + "encoding/json" + "time" +) + +type Auth struct { + Id int64 `gorm:"primaryKey"` + Method string `gorm:"unique;type:varchar(255);not null;default:'';comment:platform"` + Config string `gorm:"type:text;not null;comment:Auth Configuration"` + Enabled *bool `gorm:"type:tinyint(1);not null;default:false;comment:Is Enabled"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (Auth) TableName() string { + return "auth_method" +} + +type AppleAuthConfig struct { + TeamID string `json:"team_id"` + KeyID string `json:"key_id"` + ClientId string `json:"client_id"` + ClientSecret string `json:"client_secret"` + RedirectURL string `json:"redirect_url"` +} + +func (l *AppleAuthConfig) Marshal() string { + bytes, err := json.Marshal(l) + if err != nil { + bytes, _ = json.Marshal(new(AppleAuthConfig)) + } + return string(bytes) +} + +func (l *AppleAuthConfig) Unmarshal(data string) error { + return json.Unmarshal([]byte(data), &l) +} + +type GoogleAuthConfig struct { + ClientId string `json:"client_id"` + ClientSecret string `json:"client_secret"` + RedirectURL string `json:"redirect_url"` +} + +func (l *GoogleAuthConfig) Marshal() string { + bytes, err := json.Marshal(l) + if err != nil { + bytes, _ = json.Marshal(new(GoogleAuthConfig)) + } + return string(bytes) +} + +func (l *GoogleAuthConfig) Unmarshal(data string) error { + return json.Unmarshal([]byte(data), &l) +} + +type GithubAuthConfig struct { + ClientId string `json:"client_id"` + ClientSecret string `json:"client_secret"` + RedirectURL string `json:"redirect_url"` +} + +func (l *GithubAuthConfig) Marshal() string { + bytes, err := json.Marshal(l) + if err != nil { + bytes, _ = json.Marshal(new(GithubAuthConfig)) + } + return string(bytes) +} + +func (l *GithubAuthConfig) Unmarshal(data string) error { + return json.Unmarshal([]byte(data), &l) +} + +type FacebookAuthConfig struct { + ClientId string `json:"client_id"` + ClientSecret string `json:"client_secret"` + RedirectURL string `json:"redirect_url"` +} + +func (l *FacebookAuthConfig) Marshal() string { + bytes, err := json.Marshal(l) + if err != nil { + bytes, _ = json.Marshal(new(FacebookAuthConfig)) + } + return string(bytes) +} + +func (l *FacebookAuthConfig) Unmarshal(data string) error { + return json.Unmarshal([]byte(data), &l) +} + +type TelegramAuthConfig struct { + BotToken string `json:"bot_token"` + EnableNotify bool `json:"enable_notify"` + WebHookDomain string `json:"webhook_domain"` +} + +func (l *TelegramAuthConfig) Marshal() string { + bytes, err := json.Marshal(l) + if err != nil { + bytes, _ = json.Marshal(new(TelegramAuthConfig)) + } + return string(bytes) +} + +func (l *TelegramAuthConfig) Unmarshal(data string) error { + return json.Unmarshal([]byte(data), &l) +} + +type EmailAuthConfig struct { + Platform string `json:"platform"` + PlatformConfig interface{} `json:"platform_config"` + EnableVerify bool `json:"enable_verify"` + EnableNotify bool `json:"enable_notify"` + EnableDomainSuffix bool `json:"enable_domain_suffix"` + DomainSuffixList string `json:"domain_suffix_list"` + VerifyEmailTemplate string `json:"verify_email_template"` + ExpirationEmailTemplate string `json:"expiration_email_template"` + MaintenanceEmailTemplate string `json:"maintenance_email_template"` + TrafficExceedEmailTemplate string `json:"traffic_exceed_email_template"` +} + +func (l *EmailAuthConfig) Marshal() string { + bytes, err := json.Marshal(l) + if err != nil { + bytes, _ = json.Marshal(new(EmailAuthConfig)) + } + return string(bytes) +} + +func (l *EmailAuthConfig) Unmarshal(data string) error { + return json.Unmarshal([]byte(data), &l) +} + +// SMTPConfig Email SMTP configuration +type SMTPConfig struct { + Host string `json:"host"` + Port int `json:"port"` + User string `json:"user"` + Pass string `json:"pass"` + From string `json:"from"` + SSL bool `json:"ssl"` +} + +func (l *SMTPConfig) Marshal() string { + bytes, err := json.Marshal(l) + if err != nil { + bytes, _ = json.Marshal(new(SMTPConfig)) + } + return string(bytes) +} + +func (l *SMTPConfig) Unmarshal(data string) error { + return json.Unmarshal([]byte(data), &l) +} + +type MobileAuthConfig struct { + Platform string `json:"platform"` + PlatformConfig interface{} `json:"platform_config"` + EnableWhitelist bool `json:"enable_whitelist"` + Whitelist []string `json:"whitelist"` +} + +func (l *MobileAuthConfig) Marshal() string { + bytes, err := json.Marshal(l) + if err != nil { + bytes, _ = json.Marshal(new(MobileAuthConfig)) + } + return string(bytes) +} + +func (l *MobileAuthConfig) Unmarshal(data string) error { + return json.Unmarshal([]byte(data), &l) +} + +type AlibabaCloudConfig struct { + Access string `json:"access"` + Secret string `json:"secret"` + SignName string `json:"sign_name"` + Endpoint string `json:"endpoint"` + TemplateCode string `json:"template_code"` +} + +func (l *AlibabaCloudConfig) Marshal() string { + bytes, err := json.Marshal(l) + if err != nil { + bytes, _ = json.Marshal(new(AlibabaCloudConfig)) + } + return string(bytes) +} + +func (l *AlibabaCloudConfig) Unmarshal(data string) error { + return json.Unmarshal([]byte(data), l) +} + +type SmsbaoConfig struct { + Access string `json:"access"` + Secret string `json:"secret"` + Template string `json:"template"` +} + +func (l *SmsbaoConfig) Marshal() string { + bytes, err := json.Marshal(l) + if err != nil { + bytes, _ = json.Marshal(new(SmsbaoConfig)) + } + return string(bytes) +} + +func (l *SmsbaoConfig) Unmarshal(data string) error { + return json.Unmarshal([]byte(data), l) +} + +type AbosendConfig struct { + ApiDomain string `json:"api_domain"` + Access string `json:"access"` + Secret string `json:"secret"` + Template string `json:"template"` +} + +func (l *AbosendConfig) Marshal() string { + bytes, err := json.Marshal(l) + if err != nil { + bytes, _ = json.Marshal(new(AbosendConfig)) + } + return string(bytes) +} + +func (l *AbosendConfig) Unmarshal(data string) error { + return json.Unmarshal([]byte(data), l) +} + +type TwilioConfig struct { + Access string `json:"access"` + Secret string `json:"secret"` + PhoneNumber string `json:"phone_number"` + Template string `json:"template"` +} + +func (l *TwilioConfig) Marshal() string { + bytes, err := json.Marshal(l) + if err != nil { + bytes, _ = json.Marshal(new(TwilioConfig)) + } + return string(bytes) +} + +func (l *TwilioConfig) Unmarshal(data string) error { + return json.Unmarshal([]byte(data), l) +} + +type DeviceConfig struct { + ShowAds bool `json:"show_ads"` + OnlyRealDevice bool `json:"only_real_device"` + EnableSecurity bool `json:"enable_security"` + SecuritySecret string `json:"security_secret"` +} + +func (l *DeviceConfig) Marshal() string { + bytes, err := json.Marshal(l) + if err != nil { + bytes, _ = json.Marshal(new(DeviceConfig)) + } + return string(bytes) +} + +func (l *DeviceConfig) Unmarshal(data string) error { + return json.Unmarshal([]byte(data), l) +} diff --git a/internal/model/auth/auth_test.go b/internal/model/auth/auth_test.go new file mode 100644 index 0000000..db5dbed --- /dev/null +++ b/internal/model/auth/auth_test.go @@ -0,0 +1,30 @@ +package auth + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAlibabaCloudConfig_Marshal(t *testing.T) { + v := new(AlibabaCloudConfig) + t.Log(v.Marshal()) +} + +func TestAlibabaCloudConfig_Unmarshal(t *testing.T) { + + cfg := AlibabaCloudConfig{ + Access: "AccessKeyId", + Secret: "AccessKeySecret", + SignName: "SignName", + Endpoint: "Endpoint", + TemplateCode: "VerifyTemplateCode", + } + data := cfg.Marshal() + v := new(AlibabaCloudConfig) + err := v.Unmarshal(data) + if err != nil { + t.Fatal(err.Error()) + } + assert.Equal(t, "AccessKeyId", v.Access) +} diff --git a/internal/model/auth/default.go b/internal/model/auth/default.go new file mode 100644 index 0000000..8131d78 --- /dev/null +++ b/internal/model/auth/default.go @@ -0,0 +1,120 @@ +package auth + +import ( + "context" + "errors" + "fmt" + + "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var _ Model = (*customAuthModel)(nil) +var ( + cacheAuthIdPrefix = "cache:auth:id:" + cacheAuthMethodPrefix = "cache:auth:method:" +) + +type ( + Model interface { + authModel + customAuthLogicModel + } + authModel interface { + Insert(ctx context.Context, data *Auth) error + FindOne(ctx context.Context, id int64) (*Auth, error) + Update(ctx context.Context, data *Auth) error + Delete(ctx context.Context, id int64) error + Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + } + + customAuthModel struct { + *defaultAuthModel + } + defaultAuthModel struct { + cache.CachedConn + table string + } +) + +func newAuthModel(db *gorm.DB, c *redis.Client) *defaultAuthModel { + return &defaultAuthModel{ + CachedConn: cache.NewConn(db, c), + table: "`auth_config`", + } +} + +//nolint:unused +func (m *defaultAuthModel) batchGetCacheKeys(Auths ...*Auth) []string { + var keys []string + for _, auth := range Auths { + keys = append(keys, m.getCacheKeys(auth)...) + } + return keys + +} +func (m *defaultAuthModel) getCacheKeys(data *Auth) []string { + if data == nil { + return []string{} + } + authIdKey := fmt.Sprintf("%s%v", cacheAuthIdPrefix, data.Id) + platformKey := fmt.Sprintf("%s%s", cacheAuthMethodPrefix, data.Method) + cacheKeys := []string{ + authIdKey, + platformKey, + } + return cacheKeys +} + +func (m *defaultAuthModel) Insert(ctx context.Context, data *Auth) error { + err := m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Create(&data).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultAuthModel) FindOne(ctx context.Context, id int64) (*Auth, error) { + AuthIdKey := fmt.Sprintf("%s%v", cacheAuthIdPrefix, id) + var resp Auth + err := m.QueryCtx(ctx, &resp, AuthIdKey, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Auth{}).Where("`id` = ?", id).First(&resp).Error + }) + switch { + case err == nil: + return &resp, nil + default: + return nil, err + } +} + +func (m *defaultAuthModel) Update(ctx context.Context, data *Auth) error { + old, err := m.FindOne(ctx, data.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Save(data).Error + }, m.getCacheKeys(old)...) + return err +} + +func (m *defaultAuthModel) Delete(ctx context.Context, id int64) error { + data, err := m.FindOne(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Delete(&Auth{}, id).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultAuthModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { + return m.TransactCtx(ctx, fn) +} diff --git a/internal/model/auth/model.go b/internal/model/auth/model.go new file mode 100644 index 0000000..a67016f --- /dev/null +++ b/internal/model/auth/model.go @@ -0,0 +1,60 @@ +package auth + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +type customAuthLogicModel interface { + GetAuthListByPage(ctx context.Context) ([]*Auth, error) + FindOneByMethod(ctx context.Context, platform string) (*Auth, error) + FindAll(ctx context.Context) ([]*Auth, error) +} + +// NewModel returns a model for the database table. +func NewModel(conn *gorm.DB, c *redis.Client) Model { + return &customAuthModel{ + defaultAuthModel: newAuthModel(conn, c), + } +} + +type Filter struct { + Show *bool + Pinned *bool + Popup *bool + Search string +} + +// GetAuthListByPage get auth list by page +func (m *customAuthModel) GetAuthListByPage(ctx context.Context) ([]*Auth, error) { + var list []*Auth + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + conn = conn.Model(&Auth{}) + return conn.Find(v).Error + }) + return list, err +} + +// FindOneByMethod find one by method +func (m *customAuthModel) FindOneByMethod(ctx context.Context, method string) (*Auth, error) { + key := fmt.Sprintf("%s%s", cacheAuthMethodPrefix, method) + var data Auth + err := m.QueryCtx(ctx, &data, key, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Auth{}).Where("method = ?", method).First(v).Error + }) + + return &data, err +} + +// FindAll find all +func (m *customAuthModel) FindAll(ctx context.Context) ([]*Auth, error) { + var list []*Auth + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + conn = conn.Model(&Auth{}) + return conn.Find(v).Error + }) + return list, err +} diff --git a/internal/model/cache/constant.go b/internal/model/cache/constant.go new file mode 100644 index 0000000..5ae995c --- /dev/null +++ b/internal/model/cache/constant.go @@ -0,0 +1,52 @@ +package cache + +const ( + // UserTodayUploadTrafficCacheKey 用户当日上传流量 + UserTodayUploadTrafficCacheKey = "node:user_today_upload_traffic" + // UserTodayDownloadTrafficCacheKey 用户当日下载流量 + UserTodayDownloadTrafficCacheKey = "node:user_today_download_traffic" + // UserTodayTotalTrafficCacheKey 用户当日总流量 + UserTodayTotalTrafficCacheKey = "node:user_today_total_traffic" + // NodeTodayUploadTrafficCacheKey 节点当日上传流量 + NodeTodayUploadTrafficCacheKey = "node:node_today_upload_traffic" + // NodeTodayDownloadTrafficCacheKey 节点当日下载流量 + NodeTodayDownloadTrafficCacheKey = "node:node_today_download_traffic" + // NodeTodayTotalTrafficCacheKey 节点当日总流量 + NodeTodayTotalTrafficCacheKey = "node:node_today_total_traffic" + // UserTodayUploadTrafficRankKey 用户当日上传流量排行榜 + UserTodayUploadTrafficRankKey = "node:user_today_upload_traffic_rank" + // UserTodayDownloadTrafficRankKey 用户当日下载流量排行榜 + UserTodayDownloadTrafficRankKey = "node:user_today_download_traffic_rank" + // UserTodayTotalTrafficRankKey 用户当日总流量排行榜 + UserTodayTotalTrafficRankKey = "node:user_today_total_traffic_rank" + // NodeTodayUploadTrafficRankKey 节点当日上传流量排行榜 + NodeTodayUploadTrafficRankKey = "node:node_today_upload_traffic_rank" + // NodeTodayDownloadTrafficRankKey 节点当日下载流量排行榜 + NodeTodayDownloadTrafficRankKey = "node:node_today_download_traffic_rank" + // NodeTodayTotalTrafficRankKey 节点当日总流量排行榜 + NodeTodayTotalTrafficRankKey = "node:node_today_total_traffic_rank" + // NodeOnlineUserCacheKey 节点在线用户 + NodeOnlineUserCacheKey = "node:node_online_user:%d" + // UserOnlineIpCacheKey 用户在线IP + UserOnlineIpCacheKey = "node:user_online_ip:%d" + // AllNodeOnlineUserCacheKey 所有节点在线用户 + AllNodeOnlineUserCacheKey = "node:all_node_online_user" + // NodeStatusCacheKey 节点状态 + NodeStatusCacheKey = "node:status:%d" + // AllNodeDownloadTrafficCacheKey 所有节点下载流量 + AllNodeDownloadTrafficCacheKey = "node:all_node_download_traffic" + // AllNodeUploadTrafficCacheKey 所有节点上传流量 + AllNodeUploadTrafficCacheKey = "node:all_node_upload_traffic" + // YesterdayTotalTrafficRank 昨日节点总流量排行榜 + YesterdayNodeTotalTrafficRank = "node:yesterday_total_traffic_rank" + // YesterdayUploadTrafficRank 昨日节点上传流量排行榜 + YesterdayNodeUploadTrafficRank = "node:yesterday_upload_traffic_rank" + // YesterdayDownloadTrafficRank 昨日节点下载流量排行榜 + YesterdayNodeDownloadTrafficRank = "node:yesterday_download_traffic_rank" + // YesterdayUserTotalTrafficRank 昨日用户总流量排行榜 + YesterdayUserTotalTrafficRank = "node:yesterday_user_total_traffic_rank" + // YesterdayUserUploadTrafficRank 昨日用户上传流量排行榜 + YesterdayUserUploadTrafficRank = "node:yesterday_user_upload_traffic_rank" + // YesterdayUserDownloadTrafficRank 昨日用户下载流量排行榜 + YesterdayUserDownloadTrafficRank = "node:yesterday_user_download_traffic_rank" +) diff --git a/internal/model/cache/node.go b/internal/model/cache/node.go new file mode 100644 index 0000000..5e4d792 --- /dev/null +++ b/internal/model/cache/node.go @@ -0,0 +1,584 @@ +package cache + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "sync" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/redis/go-redis/v9" +) + +type NodeCacheClient struct { + *redis.Client + resetMutex sync.Mutex +} + +func NewNodeCacheClient(rds *redis.Client) *NodeCacheClient { + return &NodeCacheClient{ + Client: rds, + } +} + +// AddOnlineUserIP adds user's online IP +func (c *NodeCacheClient) AddOnlineUserIP(ctx context.Context, users []NodeOnlineUser) error { + if len(users) == 0 { + // No users to add + return nil + } + + // Use Pipeline to optimize Redis operations + pipe := c.Pipeline() + + // Add user online IPs and clean up expired IPs for each user + for _, user := range users { + if user.SID <= 0 || user.IP == "" { + logger.Errorf("invalid user data: uid=%d, ip=%s", user.SID, user.IP) + continue + } + + key := fmt.Sprintf(UserOnlineIpCacheKey, user.SID) + now := time.Now() + expireTime := now.Add(5 * time.Minute) + + // Clean up expired user online IPs + pipe.ZRemRangeByScore(ctx, key, "0", fmt.Sprintf("%d", now.Unix())) + pipe.ZRemRangeByScore(ctx, AllNodeOnlineUserCacheKey, "0", fmt.Sprintf("%d", now.Unix())) + + // Add or update user online IP + // XX: Only update elements that already exist + // NX: Only add new elements + _ = pipe.ZAdd(ctx, key, redis.Z{ + Score: float64(expireTime.Unix()), + Member: user.IP, + }).Err() + _ = pipe.ZAdd(ctx, AllNodeOnlineUserCacheKey, redis.Z{ + Score: float64(expireTime.Unix()), + Member: user.IP, + }).Err() + + // Set key expiration to 5 minutes (slightly longer than IP expiration) + pipe.Expire(ctx, key, 5*time.Minute) + pipe.Expire(ctx, AllNodeOnlineUserCacheKey, 5*time.Minute) + } + + // Execute all commands + _, err := pipe.Exec(ctx) + if err != nil { + return fmt.Errorf("failed to add node user online ip: %w", err) + } + return nil +} + +// GetUserOnlineIp gets user's online IPs +func (c *NodeCacheClient) GetUserOnlineIp(ctx context.Context, uid int64) ([]string, error) { + if uid <= 0 { + return nil, fmt.Errorf("invalid parameters: uid=%d", uid) + } + + // Get user's online IPs + ips, err := c.ZRevRangeByScore(ctx, fmt.Sprintf(UserOnlineIpCacheKey, uid), &redis.ZRangeBy{ + Min: "0", + Max: fmt.Sprintf("%d", time.Now().Add(5*time.Minute).Unix()), + Offset: 0, + Count: 100, + }).Result() + if err != nil { + return nil, fmt.Errorf("failed to get user online ip: %w", err) + } + return ips, nil +} + +// UpdateNodeOnlineUser updates node's online users and IPs +func (c *NodeCacheClient) UpdateNodeOnlineUser(ctx context.Context, nodeId int64, users []NodeOnlineUser) error { + if nodeId <= 0 || len(users) == 0 { + return fmt.Errorf("invalid parameters: nodeId=%d, users=%v", nodeId, users) + } + // Organize data + data := make(map[int64][]string) + for _, user := range users { + data[user.SID] = append(data[user.SID], user.IP) + } + + value, err := json.Marshal(data) + if err != nil { + return fmt.Errorf("failed to marshal data: %w", err) + } + + c.Set(ctx, fmt.Sprintf(NodeOnlineUserCacheKey, nodeId), value, time.Minute*5) + return nil +} + +// GetNodeOnlineUser gets node's online users and IPs +func (c *NodeCacheClient) GetNodeOnlineUser(ctx context.Context, nodeId int64) (map[int64][]string, error) { + if nodeId <= 0 { + return nil, fmt.Errorf("invalid parameters: nodeId=%d", nodeId) + } + value, err := c.Get(ctx, fmt.Sprintf(NodeOnlineUserCacheKey, nodeId)).Result() + if err != nil { + return nil, fmt.Errorf("failed to get node online user: %w", err) + } + var data map[int64][]string + if err := json.Unmarshal([]byte(value), &data); err != nil { + return nil, fmt.Errorf("failed to unmarshal data: %w", err) + } + return data, nil +} + +// AddUserTodayTraffic Add user's today traffic +func (c *NodeCacheClient) AddUserTodayTraffic(ctx context.Context, uid int64, upload, download int64) error { + if uid <= 0 || upload <= 0 { + return fmt.Errorf("invalid parameters: uid=%d, upload=%d", uid, upload) + } + pipe := c.Pipeline() + // User's today upload traffic + pipe.HIncrBy(ctx, UserTodayUploadTrafficCacheKey, fmt.Sprintf("%d", uid), upload) + // User's today download traffic + pipe.HIncrBy(ctx, UserTodayDownloadTrafficCacheKey, fmt.Sprintf("%d", uid), download) + // User's today total traffic + pipe.HIncrBy(ctx, UserTodayTotalTrafficCacheKey, fmt.Sprintf("%d", uid), upload+download) + // User's today traffic ranking + pipe.ZIncrBy(ctx, UserTodayUploadTrafficRankKey, float64(upload), fmt.Sprintf("%d", uid)) + pipe.ZIncrBy(ctx, UserTodayDownloadTrafficRankKey, float64(download), fmt.Sprintf("%d", uid)) + pipe.ZIncrBy(ctx, UserTodayTotalTrafficRankKey, float64(upload+download), fmt.Sprintf("%d", uid)) + + // All node upload traffic + pipe.IncrBy(ctx, AllNodeUploadTrafficCacheKey, upload) + // All node download traffic + pipe.IncrBy(ctx, AllNodeDownloadTrafficCacheKey, download) + // Execute commands + _, err := pipe.Exec(ctx) + if err != nil { + return fmt.Errorf("failed to add user today upload traffic: %w", err) + } + return nil +} + +// AddNodeTodayTraffic Add node's today traffic +func (c *NodeCacheClient) AddNodeTodayTraffic(ctx context.Context, nodeId int64, userTraffic []UserTraffic) error { + if nodeId <= 0 || len(userTraffic) == 0 { + return fmt.Errorf("invalid parameters: nodeId=%d, userTraffic=%v", nodeId, userTraffic) + } + pipe := c.Pipeline() + upload, download, total := c.calculateTraffic(userTraffic) + pipe.HIncrBy(ctx, NodeTodayUploadTrafficCacheKey, fmt.Sprintf("%d", nodeId), upload) + pipe.HIncrBy(ctx, NodeTodayDownloadTrafficCacheKey, fmt.Sprintf("%d", nodeId), download) + pipe.HIncrBy(ctx, NodeTodayTotalTrafficCacheKey, fmt.Sprintf("%d", nodeId), total) + pipe.ZIncrBy(ctx, NodeTodayUploadTrafficRankKey, float64(upload), fmt.Sprintf("%d", nodeId)) + pipe.ZIncrBy(ctx, NodeTodayDownloadTrafficRankKey, float64(download), fmt.Sprintf("%d", nodeId)) + pipe.ZIncrBy(ctx, NodeTodayTotalTrafficRankKey, float64(total), fmt.Sprintf("%d", nodeId)) + // Execute commands + _, err := pipe.Exec(ctx) + if err != nil { + return fmt.Errorf("failed to add node today upload traffic: %w", err) + } + return nil +} + +// Get user's traffic data +func (c *NodeCacheClient) getUserTrafficData(ctx context.Context, uid int64) (upload, download int64, err error) { + upload, err = c.HGet(ctx, UserTodayUploadTrafficCacheKey, fmt.Sprintf("%d", uid)).Int64() + if err != nil { + return 0, 0, fmt.Errorf("failed to get user today upload traffic: %w", err) + } + download, err = c.HGet(ctx, UserTodayDownloadTrafficCacheKey, fmt.Sprintf("%d", uid)).Int64() + if err != nil { + return 0, 0, fmt.Errorf("failed to get user today download traffic: %w", err) + } + return upload, download, nil +} + +// Get node's traffic data +func (c *NodeCacheClient) getNodeTrafficData(ctx context.Context, nodeId int64) (upload, download int64, err error) { + upload, err = c.HGet(ctx, NodeTodayUploadTrafficCacheKey, fmt.Sprintf("%d", nodeId)).Int64() + if err != nil { + return 0, 0, fmt.Errorf("failed to get node today upload traffic: %w", err) + } + download, err = c.HGet(ctx, NodeTodayDownloadTrafficCacheKey, fmt.Sprintf("%d", nodeId)).Int64() + if err != nil { + return 0, 0, fmt.Errorf("failed to get node today download traffic: %w", err) + } + return upload, download, nil +} + +// Parse ID +func (c *NodeCacheClient) parseID(member interface{}, idType string) (int64, error) { + id, err := strconv.ParseInt(member.(string), 10, 64) + if err != nil { + return 0, fmt.Errorf("failed to parse %s id %v: %w", idType, member, err) + } + return id, nil +} + +// GetUserTodayTotalTrafficRank Get user's today total traffic ranking top N +func (c *NodeCacheClient) GetUserTodayTotalTrafficRank(ctx context.Context, n int64) ([]UserTodayTrafficRank, error) { + if n <= 0 { + return nil, fmt.Errorf("invalid parameters: n=%d", n) + } + data, err := c.ZRevRangeWithScores(ctx, UserTodayTotalTrafficRankKey, 0, n-1).Result() + if err != nil { + return nil, fmt.Errorf("failed to get user today total traffic rank: %w", err) + } + users := make([]UserTodayTrafficRank, 0, len(data)) + for _, user := range data { + uid, err := c.parseID(user.Member, "user") + if err != nil { + logger.Errorf("%v", err) + continue + } + upload, download, err := c.getUserTrafficData(ctx, uid) + if err != nil { + logger.Errorf("%v", err) + continue + } + users = append(users, UserTodayTrafficRank{ + SID: uid, + Upload: upload, + Download: download, + Total: int64(user.Score), + }) + } + return users, nil +} + +// GetNodeTodayTotalTrafficRank Get node's today total traffic ranking top N +func (c *NodeCacheClient) GetNodeTodayTotalTrafficRank(ctx context.Context, n int64) ([]NodeTodayTrafficRank, error) { + if n <= 0 { + return nil, fmt.Errorf("invalid parameters: n=%d", n) + } + data, err := c.ZRevRangeWithScores(ctx, NodeTodayTotalTrafficRankKey, 0, n-1).Result() + if err != nil { + return nil, fmt.Errorf("failed to get node today total traffic rank: %w", err) + } + nodes := make([]NodeTodayTrafficRank, 0, len(data)) + for _, node := range data { + nodeId, err := c.parseID(node.Member, "node") + if err != nil { + logger.Errorf("%v", err) + continue + } + upload, download, err := c.getNodeTrafficData(ctx, nodeId) + if err != nil { + logger.Errorf("%v", err) + continue + } + nodes = append(nodes, NodeTodayTrafficRank{ + ID: nodeId, + Upload: upload, + Download: download, + Total: int64(node.Score), + }) + } + return nodes, nil +} + +// GetUserTodayUploadTrafficRank Get user's today upload traffic ranking top N +func (c *NodeCacheClient) GetUserTodayUploadTrafficRank(ctx context.Context, n int64) ([]UserTodayTrafficRank, error) { + if n <= 0 { + return nil, fmt.Errorf("invalid parameters: n=%d", n) + } + data, err := c.ZRevRangeWithScores(ctx, UserTodayUploadTrafficRankKey, 0, n-1).Result() + if err != nil { + return nil, fmt.Errorf("failed to get user today upload traffic rank: %w", err) + } + users := make([]UserTodayTrafficRank, 0, len(data)) + for _, user := range data { + uid, err := c.parseID(user.Member, "user") + if err != nil { + logger.Errorf("%v", err) + continue + } + upload, download, err := c.getUserTrafficData(ctx, uid) + if err != nil { + logger.Errorf("%v", err) + continue + } + users = append(users, UserTodayTrafficRank{ + SID: uid, + Upload: upload, + Download: download, + Total: int64(user.Score), + }) + } + return users, nil +} + +// GetUserTodayDownloadTrafficRank Get user's today download traffic ranking top N +func (c *NodeCacheClient) GetUserTodayDownloadTrafficRank(ctx context.Context, n int64) ([]UserTodayTrafficRank, error) { + if n <= 0 { + return nil, fmt.Errorf("invalid parameters: n=%d", n) + } + data, err := c.ZRevRangeWithScores(ctx, UserTodayDownloadTrafficRankKey, 0, n-1).Result() + if err != nil { + return nil, fmt.Errorf("failed to get user today download traffic rank: %w", err) + } + users := make([]UserTodayTrafficRank, 0, len(data)) + for _, user := range data { + uid, err := c.parseID(user.Member, "user") + if err != nil { + logger.Errorf("%v", err) + continue + } + upload, download, err := c.getUserTrafficData(ctx, uid) + if err != nil { + logger.Errorf("%v", err) + continue + } + users = append(users, UserTodayTrafficRank{ + SID: uid, + Upload: upload, + Download: download, + Total: int64(user.Score), + }) + } + return users, nil +} + +// GetNodeTodayUploadTrafficRank Get node's today upload traffic ranking top N +func (c *NodeCacheClient) GetNodeTodayUploadTrafficRank(ctx context.Context, n int64) ([]NodeTodayTrafficRank, error) { + if n <= 0 { + return nil, fmt.Errorf("invalid parameters: n=%d", n) + } + data, err := c.ZRevRangeWithScores(ctx, NodeTodayUploadTrafficRankKey, 0, n-1).Result() + if err != nil { + return nil, fmt.Errorf("failed to get node today upload traffic rank: %w", err) + } + nodes := make([]NodeTodayTrafficRank, 0, len(data)) + for _, node := range data { + nodeId, err := c.parseID(node.Member, "node") + if err != nil { + logger.Errorf("%v", err) + continue + } + upload, download, err := c.getNodeTrafficData(ctx, nodeId) + if err != nil { + logger.Errorf("%v", err) + continue + } + nodes = append(nodes, NodeTodayTrafficRank{ + ID: nodeId, + Upload: upload, + Download: download, + Total: int64(node.Score), + }) + } + return nodes, nil +} + +// GetNodeTodayDownloadTrafficRank Get node's today download traffic ranking top N +func (c *NodeCacheClient) GetNodeTodayDownloadTrafficRank(ctx context.Context, n int64) ([]NodeTodayTrafficRank, error) { + if n <= 0 { + return nil, fmt.Errorf("invalid parameters: n=%d", n) + } + data, err := c.ZRevRangeWithScores(ctx, NodeTodayDownloadTrafficRankKey, 0, n-1).Result() + if err != nil { + return nil, fmt.Errorf("failed to get node today download traffic rank: %w", err) + } + nodes := make([]NodeTodayTrafficRank, 0, len(data)) + for _, node := range data { + nodeId, err := c.parseID(node.Member, "node") + if err != nil { + logger.Errorf("%v", err) + continue + } + upload, download, err := c.getNodeTrafficData(ctx, nodeId) + if err != nil { + logger.Errorf("%v", err) + continue + } + nodes = append(nodes, NodeTodayTrafficRank{ + ID: nodeId, + Upload: upload, + Download: download, + Total: int64(node.Score), + }) + } + return nodes, nil +} + +// ResetTodayTrafficData Reset today's traffic data +func (c *NodeCacheClient) ResetTodayTrafficData(ctx context.Context) error { + c.resetMutex.Lock() + defer c.resetMutex.Unlock() + pipe := c.Pipeline() + pipe.Del(ctx, UserTodayUploadTrafficCacheKey) + pipe.Del(ctx, UserTodayDownloadTrafficCacheKey) + pipe.Del(ctx, UserTodayTotalTrafficCacheKey) + pipe.Del(ctx, NodeTodayUploadTrafficCacheKey) + pipe.Del(ctx, NodeTodayDownloadTrafficCacheKey) + pipe.Del(ctx, NodeTodayTotalTrafficCacheKey) + pipe.Del(ctx, UserTodayUploadTrafficRankKey) + pipe.Del(ctx, UserTodayDownloadTrafficRankKey) + pipe.Del(ctx, UserTodayTotalTrafficRankKey) + pipe.Del(ctx, NodeTodayUploadTrafficRankKey) + pipe.Del(ctx, NodeTodayDownloadTrafficRankKey) + pipe.Del(ctx, NodeTodayTotalTrafficRankKey) + pipe.Del(ctx, AllNodeDownloadTrafficCacheKey) + pipe.Del(ctx, AllNodeUploadTrafficCacheKey) + _, err := pipe.Exec(ctx) + if err != nil { + return fmt.Errorf("failed to reset today traffic data: %w", err) + } + return nil +} + +// Calculate traffic +func (c *NodeCacheClient) calculateTraffic(data []UserTraffic) (upload, download, total int64) { + for _, userTraffic := range data { + upload += userTraffic.Upload + download += userTraffic.Download + total += userTraffic.Upload + userTraffic.Download + } + return upload, download, total +} + +// GetAllNodeOnlineUser Get all node online user +func (c *NodeCacheClient) GetAllNodeOnlineUser(ctx context.Context) ([]string, error) { + users, err := c.ZRevRange(ctx, AllNodeOnlineUserCacheKey, 0, -1).Result() + if err != nil { + return nil, fmt.Errorf("failed to get all node online user: %w", err) + } + return users, nil +} + +// UpdateNodeStatus Update node status +func (c *NodeCacheClient) UpdateNodeStatus(ctx context.Context, nodeId int64, status NodeStatus) error { + // 参数验证 + if nodeId <= 0 { + return fmt.Errorf("invalid node id: %d", nodeId) + } + + // 验证状态数据 + if status.UpdatedAt <= 0 { + return fmt.Errorf("invalid status data: updated_at=%d", status.UpdatedAt) + } + + // 序列化状态数据 + value, err := json.Marshal(status) + if err != nil { + return fmt.Errorf("failed to marshal node status: %w", err) + } + + // 使用 Pipeline 优化性能 + pipe := c.Pipeline() + + // 设置状态数据 + pipe.Set(ctx, fmt.Sprintf(NodeStatusCacheKey, nodeId), value, time.Minute*5) + + // 执行命令 + _, err = pipe.Exec(ctx) + if err != nil { + return fmt.Errorf("failed to update node status: %w", err) + } + + return nil +} + +// GetNodeStatus Get node status +func (c *NodeCacheClient) GetNodeStatus(ctx context.Context, nodeId int64) (NodeStatus, error) { + status, err := c.Get(ctx, fmt.Sprintf(NodeStatusCacheKey, nodeId)).Result() + if err != nil { + return NodeStatus{}, fmt.Errorf("failed to get node status: %w", err) + } + var nodeStatus NodeStatus + if err := json.Unmarshal([]byte(status), &nodeStatus); err != nil { + return NodeStatus{}, fmt.Errorf("failed to unmarshal node status: %w", err) + } + return nodeStatus, nil +} + +// GetOnlineNodeStatusCount Get Online Node Status Count +func (c *NodeCacheClient) GetOnlineNodeStatusCount(ctx context.Context) (int64, error) { + // 获取所有节点Key + keys, err := c.Keys(ctx, "node:status:*").Result() + if err != nil { + return 0, fmt.Errorf("failed to get all node status keys: %w", err) + } + var count int64 + for _, key := range keys { + status, err := c.Get(ctx, key).Result() + if err != nil { + logger.Errorf("failed to get node status: %v", err.Error()) + continue + } + if status != "" { + count++ + } + } + return count, nil +} + +// GetAllNodeUploadTraffic Get all node upload traffic +func (c *NodeCacheClient) GetAllNodeUploadTraffic(ctx context.Context) (int64, error) { + upload, err := c.Get(ctx, AllNodeUploadTrafficCacheKey).Int64() + if err != nil { + return 0, fmt.Errorf("failed to get all node upload traffic: %w", err) + } + return upload, nil +} + +// GetAllNodeDownloadTraffic Get all node download traffic +func (c *NodeCacheClient) GetAllNodeDownloadTraffic(ctx context.Context) (int64, error) { + download, err := c.Get(ctx, AllNodeDownloadTrafficCacheKey).Int64() + if err != nil { + return 0, fmt.Errorf("failed to get all node download traffic: %w", err) + } + return download, nil +} + +// UpdateYesterdayNodeTotalTrafficRank Update yesterday node total traffic rank +func (c *NodeCacheClient) UpdateYesterdayNodeTotalTrafficRank(ctx context.Context, nodes []NodeTodayTrafficRank) error { + expireAt := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.Local).Add(time.Hour * 24) + t := time.Until(expireAt) + pipe := c.Pipeline() + value, _ := json.Marshal(nodes) + pipe.Set(ctx, YesterdayNodeTotalTrafficRank, value, t) + _, err := pipe.Exec(ctx) + if err != nil { + return fmt.Errorf("failed to update yesterday node total traffic rank: %w", err) + } + return nil +} + +// UpdateYesterdayUserTotalTrafficRank Update yesterday user total traffic rank +func (c *NodeCacheClient) UpdateYesterdayUserTotalTrafficRank(ctx context.Context, users []UserTodayTrafficRank) error { + expireAt := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.Local).Add(time.Hour * 24) + t := time.Until(expireAt) + pipe := c.Pipeline() + value, _ := json.Marshal(users) + pipe.Set(ctx, YesterdayUserTotalTrafficRank, value, t) + _, err := pipe.Exec(ctx) + if err != nil { + return fmt.Errorf("failed to update yesterday user total traffic rank: %w", err) + } + return nil +} + +// GetYesterdayNodeTotalTrafficRank Get yesterday node total traffic rank +func (c *NodeCacheClient) GetYesterdayNodeTotalTrafficRank(ctx context.Context) ([]NodeTodayTrafficRank, error) { + value, err := c.Get(ctx, YesterdayNodeTotalTrafficRank).Result() + if err != nil { + return nil, fmt.Errorf("failed to get yesterday node total traffic rank: %w", err) + } + var nodes []NodeTodayTrafficRank + if err := json.Unmarshal([]byte(value), &nodes); err != nil { + return nil, fmt.Errorf("failed to unmarshal yesterday node total traffic rank: %w", err) + } + return nodes, nil +} + +// GetYesterdayUserTotalTrafficRank Get yesterday user total traffic rank +func (c *NodeCacheClient) GetYesterdayUserTotalTrafficRank(ctx context.Context) ([]UserTodayTrafficRank, error) { + value, err := c.Get(ctx, YesterdayUserTotalTrafficRank).Result() + if err != nil { + return nil, fmt.Errorf("failed to get yesterday user total traffic rank: %w", err) + } + var users []UserTodayTrafficRank + if err := json.Unmarshal([]byte(value), &users); err != nil { + return nil, fmt.Errorf("failed to unmarshal yesterday user total traffic rank: %w", err) + } + return users, nil +} diff --git a/internal/model/cache/node_test.go b/internal/model/cache/node_test.go new file mode 100644 index 0000000..b7660ab --- /dev/null +++ b/internal/model/cache/node_test.go @@ -0,0 +1,575 @@ +package cache + +import ( + "context" + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/alicebob/miniredis/v2" + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Create a test Redis client +func newTestRedisClient(t *testing.T) *redis.Client { + mr, err := miniredis.Run() + require.NoError(t, err) + + client := redis.NewClient(&redis.Options{ + Addr: mr.Addr(), + }) + require.NoError(t, client.Ping(context.Background()).Err()) + return client +} + +// Clean up test data +func cleanupTestData(t *testing.T, client *redis.Client) { + ctx := context.Background() + keys := []string{ + UserTodayUploadTrafficCacheKey, + UserTodayDownloadTrafficCacheKey, + UserTodayTotalTrafficCacheKey, + NodeTodayUploadTrafficCacheKey, + NodeTodayDownloadTrafficCacheKey, + NodeTodayTotalTrafficCacheKey, + UserTodayUploadTrafficRankKey, + UserTodayDownloadTrafficRankKey, + UserTodayTotalTrafficRankKey, + NodeTodayUploadTrafficRankKey, + NodeTodayDownloadTrafficRankKey, + NodeTodayTotalTrafficRankKey, + } + + // Clean up all cache keys + for _, key := range keys { + require.NoError(t, client.Del(ctx, key).Err()) + } + + // Clean up user online IP cache + for uid := int64(1); uid <= 3; uid++ { + require.NoError(t, client.Del(ctx, fmt.Sprintf(UserOnlineIpCacheKey, uid)).Err()) + } + + // Clean up node online user cache + for nodeId := int64(1); nodeId <= 3; nodeId++ { + require.NoError(t, client.Del(ctx, fmt.Sprintf(NodeOnlineUserCacheKey, nodeId)).Err()) + } +} + +func TestNodeCacheClient_AddUserTodayTraffic(t *testing.T) { + client := newTestRedisClient(t) + defer cleanupTestData(t, client) + + cache := NewNodeCacheClient(client) + ctx := context.Background() + + tests := []struct { + name string + uid int64 + upload int64 + download int64 + wantErr bool + }{ + { + name: "Add traffic normally", + uid: 1, + upload: 100, + download: 200, + wantErr: false, + }, + { + name: "Invalid SID", + uid: 0, + upload: 100, + download: 200, + wantErr: true, + }, + { + name: "Invalid upload traffic", + uid: 1, + upload: 0, + download: 200, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := cache.AddUserTodayTraffic(ctx, tt.uid, tt.upload, tt.download) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + // Verify data is added correctly + upload, err := client.HGet(ctx, UserTodayUploadTrafficCacheKey, "1").Int64() + assert.NoError(t, err) + assert.Equal(t, tt.upload, upload) + + download, err := client.HGet(ctx, UserTodayDownloadTrafficCacheKey, "1").Int64() + assert.NoError(t, err) + assert.Equal(t, tt.download, download) + }) + } +} + +func TestNodeCacheClient_AddNodeTodayTraffic(t *testing.T) { + client := newTestRedisClient(t) + defer cleanupTestData(t, client) + + cache := NewNodeCacheClient(client) + ctx := context.Background() + + tests := []struct { + name string + nodeId int64 + userTraffic []UserTraffic + wantErr bool + }{ + { + name: "Add node traffic normally", + nodeId: 1, + userTraffic: []UserTraffic{ + {UID: 1, Upload: 100, Download: 200}, + {UID: 2, Upload: 300, Download: 400}, + }, + wantErr: false, + }, + { + name: "Invalid node ID", + nodeId: 0, + userTraffic: []UserTraffic{ + {UID: 1, Upload: 100, Download: 200}, + }, + wantErr: true, + }, + { + name: "Empty user traffic data", + nodeId: 1, + userTraffic: []UserTraffic{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := cache.AddNodeTodayTraffic(ctx, tt.nodeId, tt.userTraffic) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + // Verify data is added correctly + upload, err := client.HGet(ctx, NodeTodayUploadTrafficCacheKey, "1").Int64() + assert.NoError(t, err) + assert.Equal(t, int64(400), upload) // 100 + 300 + + download, err := client.HGet(ctx, NodeTodayDownloadTrafficCacheKey, "1").Int64() + assert.NoError(t, err) + assert.Equal(t, int64(600), download) // 200 + 400 + }) + } +} + +func TestNodeCacheClient_GetUserTodayTrafficRank(t *testing.T) { + client := newTestRedisClient(t) + defer cleanupTestData(t, client) + + cache := NewNodeCacheClient(client) + ctx := context.Background() + + // Prepare test data + testData := []struct { + uid int64 + upload int64 + download int64 + }{ + {1, 100, 200}, + {2, 300, 400}, + {3, 500, 600}, + } + + for _, data := range testData { + err := cache.AddUserTodayTraffic(ctx, data.uid, data.upload, data.download) + require.NoError(t, err) + } + + tests := []struct { + name string + n int64 + wantErr bool + }{ + { + name: "Get top 2 ranks", + n: 2, + wantErr: false, + }, + { + name: "Get all ranks", + n: 3, + wantErr: false, + }, + { + name: "Invalid N value", + n: 0, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ranks, err := cache.GetUserTodayTotalTrafficRank(ctx, tt.n) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Len(t, ranks, int(tt.n)) + + // Verify sorting is correct + for i := 1; i < len(ranks); i++ { + assert.GreaterOrEqual(t, ranks[i-1].Total, ranks[i].Total) + } + }) + } +} + +func TestNodeCacheClient_ResetTodayTrafficData(t *testing.T) { + client := newTestRedisClient(t) + defer cleanupTestData(t, client) + + cache := NewNodeCacheClient(client) + ctx := context.Background() + + // Prepare test data + err := cache.AddUserTodayTraffic(ctx, 1, 100, 200) + require.NoError(t, err) + err = cache.AddNodeTodayTraffic(ctx, 1, []UserTraffic{{UID: 1, Upload: 100, Download: 200}}) + require.NoError(t, err) + + // Test reset functionality + err = cache.ResetTodayTrafficData(ctx) + assert.NoError(t, err) + + // Verify data is cleared + keys := []string{ + UserTodayUploadTrafficCacheKey, + UserTodayDownloadTrafficCacheKey, + UserTodayTotalTrafficCacheKey, + NodeTodayUploadTrafficCacheKey, + NodeTodayDownloadTrafficCacheKey, + NodeTodayTotalTrafficCacheKey, + } + + for _, key := range keys { + exists, err := client.Exists(ctx, key).Result() + assert.NoError(t, err) + assert.Equal(t, int64(0), exists) + } +} + +func TestNodeCacheClient_GetNodeTodayTrafficRank(t *testing.T) { + client := newTestRedisClient(t) + defer cleanupTestData(t, client) + + cache := NewNodeCacheClient(client) + ctx := context.Background() + + // Prepare test data + testData := []struct { + nodeId int64 + traffic []UserTraffic + }{ + {1, []UserTraffic{{UID: 1, Upload: 100, Download: 200}}}, + {2, []UserTraffic{{UID: 2, Upload: 300, Download: 400}}}, + {3, []UserTraffic{{UID: 3, Upload: 500, Download: 600}}}, + } + + for _, data := range testData { + err := cache.AddNodeTodayTraffic(ctx, data.nodeId, data.traffic) + require.NoError(t, err) + } + + tests := []struct { + name string + n int64 + wantErr bool + }{ + { + name: "Get top 2 ranks", + n: 2, + wantErr: false, + }, + { + name: "Get all ranks", + n: 3, + wantErr: false, + }, + { + name: "Invalid N value", + n: 0, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ranks, err := cache.GetNodeTodayTotalTrafficRank(ctx, tt.n) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Len(t, ranks, int(tt.n)) + + // Verify sorting is correct + for i := 1; i < len(ranks); i++ { + assert.GreaterOrEqual(t, ranks[i-1].Total, ranks[i].Total) + } + }) + } +} + +func TestNodeCacheClient_AddNodeOnlineUser(t *testing.T) { + client := newTestRedisClient(t) + defer cleanupTestData(t, client) + + cache := NewNodeCacheClient(client) + ctx := context.Background() + + tests := []struct { + name string + nodeId int64 + users []NodeOnlineUser + wantErr bool + }{ + { + name: "Add online users normally", + nodeId: 1, + users: []NodeOnlineUser{ + {SID: 1, IP: "192.168.1.1"}, + {SID: 2, IP: "192.168.1.2"}, + }, + wantErr: false, + }, + { + name: "Invalid node ID", + nodeId: 0, + users: []NodeOnlineUser{ + {SID: 1, IP: "192.168.1.1"}, + }, + wantErr: false, + }, + { + name: "Empty user list", + nodeId: 1, + users: []NodeOnlineUser{}, + wantErr: false, + }, + { + name: "Add duplicate user IP", + nodeId: 1, + users: []NodeOnlineUser{ + {SID: 1, IP: "192.168.1.1"}, + {SID: 1, IP: "192.168.1.1"}, + }, + wantErr: false, + }, + { + name: "Multiple IPs for same user", + nodeId: 1, + users: []NodeOnlineUser{ + {SID: 1, IP: "192.168.1.1"}, + {SID: 1, IP: "192.168.1.2"}, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := cache.AddOnlineUserIP(ctx, tt.users) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + // Verify data is added correctly + for _, user := range tt.users { + // Get user online IPs + ips, err := cache.GetUserOnlineIp(ctx, user.SID) + assert.NoError(t, err) + assert.Contains(t, ips, user.IP) + + // Verify score is within valid range (current time to 5 minutes later) + score, err := client.ZScore(ctx, fmt.Sprintf(UserOnlineIpCacheKey, user.SID), user.IP).Result() + assert.NoError(t, err) + now := time.Now().Unix() + assert.GreaterOrEqual(t, score, float64(now)) + assert.LessOrEqual(t, score, float64(now+300)) // 5 minutes = 300 seconds + + // Verify key exists + exists, err := client.Exists(ctx, fmt.Sprintf(UserOnlineIpCacheKey, user.SID)).Result() + assert.NoError(t, err) + assert.Equal(t, int64(1), exists) + } + }) + } +} + +func TestNodeCacheClient_GetUserOnlineIp(t *testing.T) { + client := newTestRedisClient(t) + defer cleanupTestData(t, client) + + cache := NewNodeCacheClient(client) + ctx := context.Background() + + // Prepare test data + testData := []struct { + nodeId int64 + users []NodeOnlineUser + }{ + { + nodeId: 1, + users: []NodeOnlineUser{ + {SID: 1, IP: "192.168.1.1"}, + {SID: 1, IP: "192.168.1.2"}, + {SID: 2, IP: "192.168.1.3"}, + }, + }, + } + + // Add test data + for _, data := range testData { + err := cache.AddOnlineUserIP(ctx, data.users) + require.NoError(t, err) + } + + tests := []struct { + name string + uid int64 + wantErr bool + wantIPs []string + }{ + { + name: "Get existing user IPs", + uid: 1, + wantErr: false, + wantIPs: []string{"192.168.1.1", "192.168.1.2"}, + }, + { + name: "Get another user's IPs", + uid: 2, + wantErr: false, + wantIPs: []string{"192.168.1.3"}, + }, + { + name: "Get non-existent user IPs", + uid: 3, + wantErr: false, + wantIPs: []string{}, + }, + { + name: "Invalid user ID", + uid: 0, + wantErr: true, + }, + { + name: "Expired IPs should not be returned", + uid: 1, + wantErr: false, + wantIPs: []string{"192.168.1.1", "192.168.1.2"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ips, err := cache.GetUserOnlineIp(ctx, tt.uid) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.ElementsMatch(t, tt.wantIPs, ips) + + // Verify all returned IPs are valid + for _, ip := range ips { + score, err := client.ZScore(ctx, fmt.Sprintf(UserOnlineIpCacheKey, tt.uid), ip).Result() + assert.NoError(t, err) + now := time.Now().Unix() + assert.GreaterOrEqual(t, score, float64(now)) + } + }) + } +} + +func TestNodeCacheClient_UpdateNodeOnlineUser(t *testing.T) { + client := newTestRedisClient(t) + defer cleanupTestData(t, client) + + cache := NewNodeCacheClient(client) + ctx := context.Background() + + tests := []struct { + name string + nodeId int64 + users []NodeOnlineUser + wantErr bool + }{ + { + name: "Update online users normally", + nodeId: 1, + users: []NodeOnlineUser{ + {SID: 1, IP: "192.168.1.1"}, + {SID: 2, IP: "192.168.1.2"}, + }, + wantErr: false, + }, + { + name: "Invalid node ID", + nodeId: 0, + users: []NodeOnlineUser{ + {SID: 1, IP: "192.168.1.1"}, + }, + wantErr: true, + }, + { + name: "Empty user list", + nodeId: 1, + users: []NodeOnlineUser{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := cache.UpdateNodeOnlineUser(ctx, tt.nodeId, tt.users) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + // Verify data is updated correctly + data, err := client.Get(ctx, fmt.Sprintf(NodeOnlineUserCacheKey, tt.nodeId)).Result() + assert.NoError(t, err) + + var result map[int64][]string + err = json.Unmarshal([]byte(data), &result) + assert.NoError(t, err) + + // Verify data content + for _, user := range tt.users { + ips, exists := result[user.SID] + assert.True(t, exists) + assert.Contains(t, ips, user.IP) + } + }) + } +} diff --git a/internal/model/cache/types.go b/internal/model/cache/types.go new file mode 100644 index 0000000..89b6144 --- /dev/null +++ b/internal/model/cache/types.go @@ -0,0 +1,34 @@ +package cache + +type NodeOnlineUser struct { + SID int64 + IP string +} + +type NodeTodayTrafficRank struct { + ID int64 + Name string + Upload int64 + Download int64 + Total int64 +} + +type UserTodayTrafficRank struct { + SID int64 + Upload int64 + Download int64 + Total int64 +} + +type UserTraffic struct { + UID int64 + Upload int64 + Download int64 +} + +type NodeStatus struct { + Cpu float64 + Mem float64 + Disk float64 + UpdatedAt int64 +} diff --git a/internal/model/coupon/coupon.go b/internal/model/coupon/coupon.go new file mode 100644 index 0000000..18925dd --- /dev/null +++ b/internal/model/coupon/coupon.go @@ -0,0 +1,24 @@ +package coupon + +import "time" + +type Coupon struct { + Id int64 `gorm:"primaryKey"` + Name string `gorm:"type:varchar(255);not null;default:'';comment:Coupon Name"` + Code string `gorm:"type:varchar(255);not null;default:'';unique;comment:Coupon Code"` + Count int64 `gorm:"type:int;not null;default:0;comment:Count Limit"` + Type uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Coupon Type: 1: Percentage 2: Fixed Amount"` + Discount int64 `gorm:"type:int;not null;default:0;comment:Coupon Discount"` + StartTime int64 `gorm:"type:int;not null;default:0;comment:Start Time"` + ExpireTime int64 `gorm:"type:int;not null;default:0;comment:Expire Time"` + UserLimit int64 `gorm:"type:int;not null;default:0;comment:User Limit"` + Subscribe string `gorm:"type:varchar(255);not null;default:'';comment:Subscribe Limit"` + UsedCount int64 `gorm:"type:int;not null;default:0;comment:Used Count"` + Enable *bool `gorm:"type:tinyint(1);not null;default:1;comment:Enable"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (Coupon) TableName() string { + return "coupon" +} diff --git a/internal/model/coupon/default.go b/internal/model/coupon/default.go new file mode 100644 index 0000000..2abca66 --- /dev/null +++ b/internal/model/coupon/default.go @@ -0,0 +1,135 @@ +package coupon + +import ( + "context" + "errors" + "fmt" + + "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var _ Model = (*customCouponModel)(nil) +var ( + cacheCouponIdPrefix = "cache:coupon:id:" + cacheCouponCodePrefix = "cache:coupon:code:" +) + +type ( + Model interface { + couponModel + customCouponLogicModel + } + couponModel interface { + Insert(ctx context.Context, data *Coupon) error + FindOne(ctx context.Context, id int64) (*Coupon, error) + FindOneByCode(ctx context.Context, code string) (*Coupon, error) + Update(ctx context.Context, data *Coupon) error + Delete(ctx context.Context, id int64) error + Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + } + + customCouponModel struct { + *defaultCouponModel + } + defaultCouponModel struct { + cache.CachedConn + table string + } +) + +func newCouponModel(db *gorm.DB, c *redis.Client) *defaultCouponModel { + return &defaultCouponModel{ + CachedConn: cache.NewConn(db, c), + table: "`coupon`", + } +} + +//nolint:unused +func (m *defaultCouponModel) batchGetCacheKeys(Coupons ...*Coupon) []string { + var keys []string + for _, coupon := range Coupons { + keys = append(keys, m.getCacheKeys(coupon)...) + } + return keys + +} +func (m *defaultCouponModel) getCacheKeys(data *Coupon) []string { + if data == nil { + return []string{} + } + couponIdKey := fmt.Sprintf("%s%v", cacheCouponIdPrefix, data.Id) + couponCodeKey := fmt.Sprintf("%s%v", cacheCouponCodePrefix, data.Code) + cacheKeys := []string{ + couponIdKey, + couponCodeKey, + } + return cacheKeys +} + +func (m *defaultCouponModel) Insert(ctx context.Context, data *Coupon) error { + err := m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Create(&data).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultCouponModel) FindOne(ctx context.Context, id int64) (*Coupon, error) { + CouponIdKey := fmt.Sprintf("%s%v", cacheCouponIdPrefix, id) + var resp Coupon + err := m.QueryCtx(ctx, &resp, CouponIdKey, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Coupon{}).Where("`id` = ?", id).First(&resp).Error + }) + switch { + case err == nil: + return &resp, nil + default: + return nil, err + } +} + +func (m *defaultCouponModel) FindOneByCode(ctx context.Context, code string) (*Coupon, error) { + CouponCodeKey := fmt.Sprintf("%s%v", cacheCouponCodePrefix, code) + var resp Coupon + err := m.QueryCtx(ctx, &resp, CouponCodeKey, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Coupon{}).Where("`code` = ?", code).First(&resp).Error + }) + switch { + case err == nil: + return &resp, nil + default: + return nil, err + } +} + +func (m *defaultCouponModel) Update(ctx context.Context, data *Coupon) error { + old, err := m.FindOne(ctx, data.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Save(data).Error + }, m.getCacheKeys(old)...) + return err +} + +func (m *defaultCouponModel) Delete(ctx context.Context, id int64) error { + data, err := m.FindOne(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Delete(&Coupon{}, id).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultCouponModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { + return m.TransactCtx(ctx, fn) +} diff --git a/internal/model/coupon/model.go b/internal/model/coupon/model.go new file mode 100644 index 0000000..8c1ad57 --- /dev/null +++ b/internal/model/coupon/model.go @@ -0,0 +1,55 @@ +package coupon + +import ( + "context" + + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +type customCouponLogicModel interface { + UpdateCount(ctx context.Context, code string) error + QueryCouponListByPage(ctx context.Context, page, size int, subscribe int64, search string) (total int64, list []*Coupon, err error) + BatchDelete(ctx context.Context, ids []int64) error +} + +// NewModel returns a model for the database table. +func NewModel(conn *gorm.DB, c *redis.Client) Model { + return &customCouponModel{ + defaultCouponModel: newCouponModel(conn, c), + } +} + +// QueryCouponListByPage query coupon list by page +func (m *customCouponModel) QueryCouponListByPage(ctx context.Context, page, size int, subscribe int64, search string) (total int64, list []*Coupon, err error) { + err = m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + db := conn.Model(&Coupon{}) + if subscribe != 0 { + db = db.Where("FIND_IN_SET(?, subscribe)", subscribe) + } + if search != "" { + db = db.Where("name like ? or code like ?", "%"+search+"%", "%"+search+"%") + } + return db.Count(&total).Limit(size).Offset((page - 1) * size).Find(v).Error + }) + return total, list, err +} + +func (m *customCouponModel) BatchDelete(ctx context.Context, ids []int64) error { + var err error + for _, id := range ids { + if err = m.Delete(ctx, id); err != nil { + return err + } + } + return nil +} + +func (m *customCouponModel) UpdateCount(ctx context.Context, code string) error { + data, err := m.FindOneByCode(ctx, code) + if err != nil { + return err + } + data.UsedCount++ + return m.Update(ctx, data) +} diff --git a/internal/model/document/default.go b/internal/model/document/default.go new file mode 100644 index 0000000..921fdcb --- /dev/null +++ b/internal/model/document/default.go @@ -0,0 +1,117 @@ +package document + +import ( + "context" + "errors" + "fmt" + + "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var _ Model = (*customDocumentModel)(nil) +var ( + cacheDocumentIdPrefix = "cache:document:id:" +) + +type ( + Model interface { + documentModel + customDocumentLogicModel + } + documentModel interface { + Insert(ctx context.Context, data *Document) error + FindOne(ctx context.Context, id int64) (*Document, error) + Update(ctx context.Context, data *Document) error + Delete(ctx context.Context, id int64) error + Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + } + + customDocumentModel struct { + *defaultDocumentModel + } + defaultDocumentModel struct { + cache.CachedConn + table string + } +) + +func newDocumentModel(db *gorm.DB, c *redis.Client) *defaultDocumentModel { + return &defaultDocumentModel{ + CachedConn: cache.NewConn(db, c), + table: "`document`", + } +} + +//nolint:unused +func (m *defaultDocumentModel) batchGetCacheKeys(Documents ...*Document) []string { + var keys []string + for _, document := range Documents { + keys = append(keys, m.getCacheKeys(document)...) + } + return keys + +} +func (m *defaultDocumentModel) getCacheKeys(data *Document) []string { + if data == nil { + return []string{} + } + documentIdKey := fmt.Sprintf("%s%v", cacheDocumentIdPrefix, data.Id) + cacheKeys := []string{ + documentIdKey, + } + return cacheKeys +} + +func (m *defaultDocumentModel) Insert(ctx context.Context, data *Document) error { + err := m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Create(&data).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultDocumentModel) FindOne(ctx context.Context, id int64) (*Document, error) { + DocumentIdKey := fmt.Sprintf("%s%v", cacheDocumentIdPrefix, id) + var resp Document + err := m.QueryCtx(ctx, &resp, DocumentIdKey, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Document{}).Where("`id` = ?", id).First(&resp).Error + }) + switch { + case err == nil: + return &resp, nil + default: + return nil, err + } +} + +func (m *defaultDocumentModel) Update(ctx context.Context, data *Document) error { + old, err := m.FindOne(ctx, data.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Save(data).Error + }, m.getCacheKeys(old)...) + return err +} + +func (m *defaultDocumentModel) Delete(ctx context.Context, id int64) error { + data, err := m.FindOne(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Delete(&Document{}, id).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultDocumentModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { + return m.TransactCtx(ctx, fn) +} diff --git a/internal/model/document/document.go b/internal/model/document/document.go new file mode 100644 index 0000000..9af2567 --- /dev/null +++ b/internal/model/document/document.go @@ -0,0 +1,17 @@ +package document + +import "time" + +type Document struct { + Id int64 `gorm:"primaryKey"` + Title string `gorm:"type:varchar(255);not null;default:'';comment:Document Title"` + Content string `gorm:"type:text;comment:Document Content"` + Tags string `gorm:"type:varchar(255);not null;default:'';comment:Document Tags"` + Show *bool `gorm:"type:tinyint(1);not null;default:1;comment:Show"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (Document) TableName() string { + return "document" +} diff --git a/internal/model/document/model.go b/internal/model/document/model.go new file mode 100644 index 0000000..039e50b --- /dev/null +++ b/internal/model/document/model.go @@ -0,0 +1,58 @@ +package document + +import ( + "context" + + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +type customDocumentLogicModel interface { + QueryDocumentDetail(ctx context.Context, id int64) (*Document, error) + QueryDocumentList(ctx context.Context, page, size int, tag string, search string) (int64, []*Document, error) + GetDocumentListByAll(ctx context.Context) (int64, []*Document, error) +} + +// NewModel returns a model for the database table. +func NewModel(conn *gorm.DB, c *redis.Client) Model { + return &customDocumentModel{ + defaultDocumentModel: newDocumentModel(conn, c), + } +} + +// QueryDocumentDetail queries the details of a document. +func (m *customDocumentModel) QueryDocumentDetail(ctx context.Context, id int64) (*Document, error) { + var data Document + err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Document{}).Preload("Group").Where("id = ?", id).Find(v).Error + }) + return &data, err +} + +// QueryDocumentList queries a list of documents. +func (m *customDocumentModel) QueryDocumentList(ctx context.Context, page, size int, tag string, search string) (int64, []*Document, error) { + var data []*Document + var total int64 + err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { + db := conn.Model(&Document{}) + if tag != "" { + db = db.Where("FIND_IN_SET(?, tags)", tag) + } + if search != "" { + db = db.Where("title LIKE ? OR content LIKE ?", "%"+search+"%", "%"+search+"%") + } + return db.Count(&total).Offset((page - 1) * size).Limit(size).Find(v).Error + }) + return total, data, err +} + +// GetDocumentListByAll queries a list of documents. +func (m *customDocumentModel) GetDocumentListByAll(ctx context.Context) (int64, []*Document, error) { + var data []*Document + var total int64 + show := true + err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Document{}).Where("`show` = ?", &show).Count(&total).Find(v).Error + }) + return total, data, err +} diff --git a/internal/model/log/default.go b/internal/model/log/default.go new file mode 100644 index 0000000..5b8d731 --- /dev/null +++ b/internal/model/log/default.go @@ -0,0 +1,79 @@ +package log + +import ( + "context" + + "gorm.io/gorm" +) + +var _ Model = (*customLogModel)(nil) + +type ( + Model interface { + messageLogModel + } + messageLogModel interface { + InsertMessageLog(ctx context.Context, data *MessageLog) error + FindOneMessageLog(ctx context.Context, id int64) (*MessageLog, error) + UpdateMessageLog(ctx context.Context, data *MessageLog) error + DeleteMessageLog(ctx context.Context, id int64) error + FindMessageLogList(ctx context.Context, page, size int, filter MessageLogFilterParams) (int64, []*MessageLog, error) + } + + customLogModel struct { + *defaultLogModel + } + defaultLogModel struct { + Connection *gorm.DB + } +) + +func newLogModel(db *gorm.DB) *defaultLogModel { + return &defaultLogModel{ + Connection: db, + } +} + +func (m *defaultLogModel) InsertMessageLog(ctx context.Context, data *MessageLog) error { + return m.Connection.WithContext(ctx).Create(&data).Error +} + +func (m *defaultLogModel) FindOneMessageLog(ctx context.Context, id int64) (*MessageLog, error) { + var resp MessageLog + err := m.Connection.WithContext(ctx).Model(&MessageLog{}).Where("`id` = ?", id).First(&resp).Error + return &resp, err +} + +func (m *defaultLogModel) UpdateMessageLog(ctx context.Context, data *MessageLog) error { + return m.Connection.WithContext(ctx).Model(&MessageLog{}).Where("id = ?", data.Id).Updates(data).Error +} + +func (m *defaultLogModel) DeleteMessageLog(ctx context.Context, id int64) error { + return m.Connection.WithContext(ctx).Model(&MessageLog{}).Where("id = ?", id).Delete(&MessageLog{}).Error +} + +func (m *defaultLogModel) FindMessageLogList(ctx context.Context, page, size int, filter MessageLogFilterParams) (int64, []*MessageLog, error) { + var list []*MessageLog + var total int64 + conn := m.Connection.WithContext(ctx).Model(&MessageLog{}) + if filter.Type != "" { + conn = conn.Where("`type` = ?", filter.Type) + } + if filter.Platform != "" { + conn = conn.Where("`platform` = ?", filter.Platform) + } + if filter.To != "" { + conn = conn.Where("`to` LIKE ?", "%"+filter.To+"%") + } + if filter.Subject != "" { + conn = conn.Where("`subject` LIKE ?", "%"+filter.Subject+"%") + } + if filter.Content != "" { + conn = conn.Where("`content` = ?", "%"+filter.Content+"%") + } + if filter.Status > 0 { + conn = conn.Where("`status` = ?", filter.Status) + } + err := conn.Count(&total).Offset((page - 1) * size).Limit(size).Find(&list).Error + return total, list, err +} diff --git a/internal/model/log/log.go b/internal/model/log/log.go new file mode 100644 index 0000000..af66746 --- /dev/null +++ b/internal/model/log/log.go @@ -0,0 +1,45 @@ +package log + +import "time" + +type MessageType int + +const ( + Email MessageType = iota + 1 + Mobile +) + +func (t MessageType) String() string { + switch t { + case Email: + return "email" + case Mobile: + return "mobile" + } + return "unknown" +} + +type MessageLog struct { + Id int64 `gorm:"primaryKey"` + Type string `gorm:"type:varchar(50);not null;default:'email';comment:Message Type"` + Platform string `gorm:"type:varchar(50);not null;default:'smtp';comment:Platform"` + To string `gorm:"type:text;not null;comment:To"` + Subject string `gorm:"type:varchar(255);not null;default:'';comment:Subject"` + Content string `gorm:"type:text;comment:Content"` + Status int `gorm:"type:tinyint(1);not null;default:0;comment:Status"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (m *MessageLog) TableName() string { + return "message_log" +} + +type MessageLogFilterParams struct { + Type string + Platform string + To string + Subject string + Content string + Status int +} diff --git a/internal/model/log/model.go b/internal/model/log/model.go new file mode 100644 index 0000000..8bacf15 --- /dev/null +++ b/internal/model/log/model.go @@ -0,0 +1,9 @@ +package log + +import ( + "gorm.io/gorm" +) + +func NewModel(conn *gorm.DB) Model { + return newLogModel(conn) +} diff --git a/internal/model/order/default.go b/internal/model/order/default.go new file mode 100644 index 0000000..6f83585 --- /dev/null +++ b/internal/model/order/default.go @@ -0,0 +1,142 @@ +package order + +import ( + "context" + "errors" + "fmt" + + "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var _ Model = (*customOrderModel)(nil) +var ( + cacheOrderIdPrefix = "cache:order:id:" + cacheOrderNoPrefix = "cache:order:no:" +) + +type ( + Model interface { + orderModel + customOrderLogicModel + } + orderModel interface { + Insert(ctx context.Context, data *Order, tx ...*gorm.DB) error + FindOne(ctx context.Context, id int64) (*Order, error) + FindOneByOrderNo(ctx context.Context, orderNo string) (*Order, error) + Update(ctx context.Context, data *Order, tx ...*gorm.DB) error + Delete(ctx context.Context, id int64, tx ...*gorm.DB) error + Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + } + + customOrderModel struct { + *defaultOrderModel + } + defaultOrderModel struct { + cache.CachedConn + table string + } +) + +func newOrderModel(db *gorm.DB, c *redis.Client) *defaultOrderModel { + return &defaultOrderModel{ + CachedConn: cache.NewConn(db, c), + table: "`order`", + } +} + +//nolint:unused +func (m *defaultOrderModel) batchGetCacheKeys(Orders ...*Order) []string { + var keys []string + for _, order := range Orders { + keys = append(keys, m.getCacheKeys(order)...) + } + return keys + +} +func (m *defaultOrderModel) getCacheKeys(data *Order) []string { + if data == nil { + return []string{} + } + orderIdKey := fmt.Sprintf("%s%v", cacheOrderIdPrefix, data.Id) + orderNoKey := fmt.Sprintf("%s%v", cacheOrderNoPrefix, data.OrderNo) + cacheKeys := []string{ + orderIdKey, + orderNoKey, + } + return cacheKeys +} + +func (m *defaultOrderModel) Insert(ctx context.Context, data *Order, tx ...*gorm.DB) error { + err := m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Create(&data).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultOrderModel) FindOne(ctx context.Context, id int64) (*Order, error) { + OrderIdKey := fmt.Sprintf("%s%v", cacheOrderIdPrefix, id) + var resp Order + err := m.QueryCtx(ctx, &resp, OrderIdKey, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Order{}).Where("`id` = ?", id).First(&resp).Error + }) + switch { + case err == nil: + return &resp, nil + default: + return nil, err + } +} + +func (m *defaultOrderModel) FindOneByOrderNo(ctx context.Context, orderNo string) (*Order, error) { + OrderNoKey := fmt.Sprintf("%s%v", cacheOrderNoPrefix, orderNo) + var resp Order + err := m.QueryCtx(ctx, &resp, OrderNoKey, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Order{}).Where("`order_no` = ?", orderNo).First(&resp).Error + }) + switch { + case err == nil: + return &resp, nil + default: + return nil, err + } +} + +func (m *defaultOrderModel) Update(ctx context.Context, data *Order, tx ...*gorm.DB) error { + old, err := m.FindOne(ctx, data.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Save(data).Error + }, m.getCacheKeys(old)...) + return err +} + +func (m *defaultOrderModel) Delete(ctx context.Context, id int64, tx ...*gorm.DB) error { + data, err := m.FindOne(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Delete(&Order{}, id).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultOrderModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { + return m.TransactCtx(ctx, fn) +} diff --git a/internal/model/order/model.go b/internal/model/order/model.go new file mode 100644 index 0000000..9909d80 --- /dev/null +++ b/internal/model/order/model.go @@ -0,0 +1,224 @@ +package order + +import ( + "context" + "time" + + "github.com/perfect-panel/ppanel-server/internal/model/payment" + + "github.com/perfect-panel/ppanel-server/internal/model/subscribe" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +type Details struct { + Id int64 `gorm:"primaryKey"` + ParentId int64 `gorm:"type:bigint;default:null;comment:Parent Order Id"` + SubOrders []*Order `gorm:"foreignKey:ParentId;references:Id"` + UserId int64 `gorm:"type:bigint;not null;default:0;comment:User Id"` + OrderNo string `gorm:"type:varchar(255);not null;default:'';unique;comment:Order No"` + Type uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Order Type: 1: Subscribe, 2: Renewal, 3: ResetTraffic, 4: Recharge"` + Quantity int64 `gorm:"type:bigint;not null;default:1;comment:Quantity"` + Price int64 `gorm:"type:int;not null;default:0;comment:Original price"` + Amount int64 `gorm:"type:int;not null;default:0;comment:Order Amount"` + Discount int64 `gorm:"type:int;not null;default:0;comment:Order Discount"` + Coupon string `gorm:"type:varchar(255);default:null;comment:Coupon"` + CouponDiscount int64 `gorm:"type:int;not null;default:0;comment:Coupon Discount"` + PaymentId int64 `gorm:"type:bigint;not null;default:0;comment:Payment Id"` + Payment *payment.Payment `gorm:"foreignKey:PaymentId;references:Id"` + Method string `gorm:"type:varchar(255);not null;default:'';comment:Payment Method"` + FeeAmount int64 `gorm:"type:int;not null;default:0;comment:Fee Amount"` + TradeNo string `gorm:"type:varchar(255);default:null;comment:Trade No"` + GiftAmount int64 `gorm:"type:int;not null;default:0;comment:User Gift Amount"` + Commission int64 `gorm:"type:int;not null;default:0;comment:Order Commission"` + Status uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Order Status: 1: Pending, 2: Paid, 3: Failed"` + SubscribeId int64 `gorm:"type:bigint;not null;default:0;comment:Subscribe Id"` + SubscribeToken string `gorm:"type:varchar(255);default:null;comment:Renewal Subscribe Token"` + Subscribe *subscribe.Subscribe `gorm:"foreignKey:SubscribeId;references:Id"` + IsNew bool `gorm:"type:tinyint(1);not null;default:0;comment:Is New Order"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +type customOrderLogicModel interface { + UpdateOrderStatus(ctx context.Context, orderNo string, status uint8, tx ...*gorm.DB) error + QueryOrderListByPage(ctx context.Context, page, size int, status uint8, user, subscribe int64, search string) (int64, []*Details, error) + FindOneDetails(ctx context.Context, id int64) (*Details, error) + FindOneDetailsByOrderNo(ctx context.Context, orderNo string) (*Details, error) + QueryMonthlyOrders(ctx context.Context, date time.Time) (OrdersTotal, error) + QueryDateOrders(ctx context.Context, date time.Time) (OrdersTotal, error) + QueryPendingOrders(ctx context.Context) ([]*Order, error) + QueryTotalOrders(ctx context.Context) (OrdersTotal, error) + QueryMonthlyUserCounts(ctx context.Context, date time.Time) (int64, int64, error) + QueryDateUserCounts(ctx context.Context, date time.Time) (int64, int64, error) + IsUserEligibleForNewOrder(ctx context.Context, userID int64) (bool, error) +} + +// NewModel returns a model for the database table. +func NewModel(conn *gorm.DB, c *redis.Client) Model { + return &customOrderModel{ + defaultOrderModel: newOrderModel(conn, c), + } +} + +// QueryOrderListByPage Query order list by page +func (m *customOrderModel) QueryOrderListByPage(ctx context.Context, page, size int, status uint8, user, subscribe int64, search string) (int64, []*Details, error) { + var list []*Details + var total int64 + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + conn = conn.Model(&Order{}) + if status > 0 { + conn = conn.Where("status = ?", status) + } + if user > 0 { + conn = conn.Where("user_id = ?", user) + } + if subscribe > 0 { + conn = conn.Where("subscribe_id = ?", subscribe) + } + if search != "" { + conn = conn.Where("order_no like ? or trade_no like ? or coupon like ?", "%"+search+"%", "%"+search+"%", "%"+search+"%") + } + return conn.Order("id desc").Preload("Subscribe").Preload("Payment").Count(&total).Offset((page - 1) * size).Limit(size).Find(v).Error + }) + return total, list, err +} + +// UpdateOrderStatus Update order status +func (m *customOrderModel) UpdateOrderStatus(ctx context.Context, orderNo string, status uint8, tx ...*gorm.DB) error { + orderInfo, err := m.FindOneByOrderNo(ctx, orderNo) + if err != nil { + return err + } + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Model(&Order{}).Where("order_no = ?", orderNo).Update("status", status).Error + }, m.getCacheKeys(orderInfo)...) +} + +// FindOneDetailsByOrderNo Find order details by order number +func (m *customOrderModel) FindOneDetailsByOrderNo(ctx context.Context, orderNo string) (*Details, error) { + var orderInfo Details + err := m.QueryNoCacheCtx(ctx, &orderInfo, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Order{}).Where("order_no = ?", orderNo).Preload("Subscribe").Preload("Payment").First(v).Error + }) + return &orderInfo, err +} + +func (m *customOrderModel) FindOneDetails(ctx context.Context, id int64) (*Details, error) { + var orderInfo Details + err := m.QueryNoCacheCtx(ctx, &orderInfo, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Order{}). + Where("id = ?", id). + Preload("Subscribe"). + Preload("SubOrders"). + First(v).Error + }) + return &orderInfo, err +} + +func (m *customOrderModel) QueryMonthlyOrders(ctx context.Context, date time.Time) (OrdersTotal, error) { + firstDay := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, date.Location()) + lastDay := firstDay.AddDate(0, 1, 0).Add(-time.Nanosecond) + var result OrdersTotal + err := m.QueryNoCacheCtx(ctx, &result, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Order{}). + Where("status IN ? AND created_at BETWEEN ? AND ? AND method != ?", []int64{2, 5}, firstDay, lastDay, "balance"). + Select( + "SUM(amount) as amount_total, " + + "SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) as new_order_amount, " + + "SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) as renewal_order_amount", + ). + Scan(v).Error + }) + return result, err +} + +// QueryDateOrders Query orders by date +func (m *customOrderModel) QueryDateOrders(ctx context.Context, date time.Time) (OrdersTotal, error) { + start := date.Truncate(24 * time.Hour) + end := start.Add(24 * time.Hour).Add(-time.Nanosecond) + var result OrdersTotal + err := m.QueryNoCacheCtx(ctx, &result, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Order{}). + Where("status IN ? AND created_at BETWEEN ? AND ? AND method != ?", []int64{2, 5}, start, end, "balance"). + Select( + "SUM(amount) as amount_total, " + + "SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) as new_order_amount, " + + "SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) as renewal_order_amount", + ). + Scan(v).Error + }) + return result, err +} + +func (m *customOrderModel) QueryTotalOrders(ctx context.Context) (OrdersTotal, error) { + var result OrdersTotal + err := m.QueryNoCacheCtx(ctx, &result, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Order{}). + Where("status IN ? AND method != ?", []int64{2, 5}, "balance"). + Select( + "SUM(amount) as amount_total, " + + "SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) as new_order_amount, " + + "SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) as renewal_order_amount", + ). + Scan(v).Error + }) + return result, err +} + +func (m *customOrderModel) QueryMonthlyUserCounts(ctx context.Context, date time.Time) (int64, int64, error) { + firstDay := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, date.Location()) + lastDay := firstDay.AddDate(0, 1, -1) + + var newUsers int64 + var renewalUsers int64 + err := m.QueryNoCacheCtx(ctx, nil, func(conn *gorm.DB, _ interface{}) error { + return conn.Model(&Order{}). + Where("status IN ? AND created_at BETWEEN ? AND ? AND method != ?", []int64{2, 5}, firstDay, lastDay, "balance"). + Select( + "COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) as new_users, "+ + "COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) as renewal_users"). + Row().Scan(&newUsers, &renewalUsers) + }) + return newUsers, renewalUsers, err +} + +func (m *customOrderModel) QueryDateUserCounts(ctx context.Context, date time.Time) (int64, int64, error) { + start := date.Truncate(24 * time.Hour) + end := start.Add(24 * time.Hour).Add(-time.Nanosecond) + + var newUsers int64 + var renewalUsers int64 + err := m.QueryNoCacheCtx(ctx, nil, func(conn *gorm.DB, _ interface{}) error { + return conn.Model(&Order{}). + Where("status IN ? AND created_at BETWEEN ? AND ? AND method != ?", []int64{2, 5}, start, end, "balance"). + Select( + "COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) as new_users, "+ + "COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) as renewal_users"). + Row().Scan(&newUsers, &renewalUsers) + }) + return newUsers, renewalUsers, err +} + +func (m *customOrderModel) IsUserEligibleForNewOrder(ctx context.Context, userID int64) (bool, error) { + var count int64 + err := m.QueryNoCacheCtx(ctx, nil, func(conn *gorm.DB, _ interface{}) error { + return conn.Model(&Order{}). + Where("user_id = ? AND status IN ?", userID, []int64{2, 5}). + Count(&count).Error + }) + return count == 0, err +} + +func (m *customOrderModel) QueryPendingOrders(ctx context.Context) ([]*Order, error) { + var orderInfo []*Order + err := m.QueryNoCacheCtx(ctx, &orderInfo, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Order{}). + Where("status = ?", 1). + Find(v).Error + }) + return orderInfo, err +} diff --git a/internal/model/order/order.go b/internal/model/order/order.go new file mode 100644 index 0000000..8e10b00 --- /dev/null +++ b/internal/model/order/order.go @@ -0,0 +1,39 @@ +package order + +import "time" + +type Order struct { + Id int64 `gorm:"primaryKey"` + ParentId int64 `gorm:"type:bigint;default:null;comment:Parent Order Id"` + UserId int64 `gorm:"type:bigint;not null;default:0;comment:User Id"` + OrderNo string `gorm:"type:varchar(255);not null;default:'';unique;comment:Order No"` + Type uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Order Type: 1: Subscribe, 2: Renewal, 3: ResetTraffic, 4: Recharge"` + Quantity int64 `gorm:"type:bigint;not null;default:1;comment:Quantity"` + Price int64 `gorm:"type:int;not null;default:0;comment:Original price"` + Amount int64 `gorm:"type:int;not null;default:0;comment:Order Amount"` + GiftAmount int64 `gorm:"type:int;not null;default:0;comment:User Gift Amount"` + Discount int64 `gorm:"type:int;not null;default:0;comment:Discount Amount"` + Coupon string `gorm:"type:varchar(255);default:null;comment:Coupon"` + CouponDiscount int64 `gorm:"type:int;not null;default:0;comment:Coupon Discount Amount"` + Commission int64 `gorm:"type:int;not null;default:0;comment:Order Commission"` + PaymentId int64 `gorm:"type:bigint;not null;default:0;comment:Payment Method Id"` + Method string `gorm:"type:varchar(255);not null;default:'';comment:Payment Method"` + FeeAmount int64 `gorm:"type:int;not null;default:0;comment:Fee Amount"` + TradeNo string `gorm:"type:varchar(255);default:null;comment:Trade No"` + Status uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Order Status: 1: Pending, 2: Paid, 3:Close, 4: Failed, 5:Finished;"` + SubscribeId int64 `gorm:"type:bigint;not null;default:0;comment:Subscribe Id"` + SubscribeToken string `gorm:"type:varchar(255);default:null;comment:Renewal Subscribe Token"` + IsNew bool `gorm:"type:tinyint(1);not null;default:0;comment:Is New Order"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +type OrdersTotal struct { + AmountTotal int64 + NewOrderAmount int64 + RenewalOrderAmount int64 +} + +func (Order) TableName() string { + return "order" +} diff --git a/internal/model/payment/default.go b/internal/model/payment/default.go new file mode 100644 index 0000000..cd4afdd --- /dev/null +++ b/internal/model/payment/default.go @@ -0,0 +1,120 @@ +package payment + +import ( + "context" + "errors" + "fmt" + + "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var _ Model = (*customPaymentModel)(nil) +var ( + cachePaymentIdPrefix = "cache:payment:id:" + cachePaymentTokenPrefix = "cache:payment:token:" +) + +type ( + Model interface { + paymentModel + customPaymentLogicModel + } + paymentModel interface { + Insert(ctx context.Context, data *Payment) error + FindOne(ctx context.Context, id int64) (*Payment, error) + Update(ctx context.Context, data *Payment) error + Delete(ctx context.Context, id int64) error + Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + } + + customPaymentModel struct { + *defaultPaymentModel + } + defaultPaymentModel struct { + cache.CachedConn + table string + } +) + +func newPaymentModel(db *gorm.DB, c *redis.Client) *defaultPaymentModel { + return &defaultPaymentModel{ + CachedConn: cache.NewConn(db, c), + table: "`Payment`", + } +} + +//nolint:unused +func (m *defaultPaymentModel) batchGetCacheKeys(Payments ...*Payment) []string { + var keys []string + for _, payment := range Payments { + keys = append(keys, m.getCacheKeys(payment)...) + } + return keys + +} +func (m *defaultPaymentModel) getCacheKeys(data *Payment) []string { + if data == nil { + return []string{} + } + paymentIdKey := fmt.Sprintf("%s%v", cachePaymentIdPrefix, data.Id) + paymentNameKey := fmt.Sprintf("%s%v", cachePaymentTokenPrefix, data.Token) + cacheKeys := []string{ + paymentIdKey, + paymentNameKey, + } + return cacheKeys +} + +func (m *defaultPaymentModel) Insert(ctx context.Context, data *Payment) error { + err := m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Create(&data).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultPaymentModel) FindOne(ctx context.Context, id int64) (*Payment, error) { + PaymentIdKey := fmt.Sprintf("%s%v", cachePaymentIdPrefix, id) + var resp Payment + err := m.QueryCtx(ctx, &resp, PaymentIdKey, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Payment{}).Where("`id` = ?", id).First(&resp).Error + }) + switch { + case err == nil: + return &resp, nil + default: + return nil, err + } +} + +func (m *defaultPaymentModel) Update(ctx context.Context, data *Payment) error { + old, err := m.FindOne(ctx, data.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Save(data).Error + }, m.getCacheKeys(old)...) + return err +} + +func (m *defaultPaymentModel) Delete(ctx context.Context, id int64) error { + data, err := m.FindOne(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Delete(&Payment{}, id).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultPaymentModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { + return m.TransactCtx(ctx, fn) +} diff --git a/internal/model/payment/model.go b/internal/model/payment/model.go new file mode 100644 index 0000000..e273ca3 --- /dev/null +++ b/internal/model/payment/model.go @@ -0,0 +1,69 @@ +package payment + +import ( + "context" + + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +type customPaymentLogicModel interface { + FindOneByPaymentToken(ctx context.Context, token string) (*Payment, error) + FindAll(ctx context.Context) ([]*Payment, error) + FindListByPage(ctx context.Context, page, size int, req *Filter) (int64, []*Payment, error) + FindAvailableMethods(ctx context.Context) ([]*Payment, error) +} + +// NewModel returns a model for the database table. +func NewModel(conn *gorm.DB, c *redis.Client) Model { + return &customPaymentModel{ + defaultPaymentModel: newPaymentModel(conn, c), + } +} + +func (m *customPaymentModel) FindOneByPaymentToken(ctx context.Context, token string) (*Payment, error) { + var resp *Payment + key := cachePaymentTokenPrefix + token + err := m.QueryCtx(ctx, &resp, key, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Payment{}).Where("token = ?", token).First(v).Error + }) + return resp, err +} + +func (m *customPaymentModel) FindAll(ctx context.Context) ([]*Payment, error) { + var resp []*Payment + err := m.QueryNoCacheCtx(ctx, &resp, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Payment{}).Find(v).Error + }) + return resp, err +} + +func (m *customPaymentModel) FindAvailableMethods(ctx context.Context) ([]*Payment, error) { + var resp []*Payment + err := m.QueryNoCacheCtx(ctx, &resp, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Payment{}).Where("enable = ?", true).Find(v).Error + }) + return resp, err +} + +func (m *customPaymentModel) FindListByPage(ctx context.Context, page, size int, req *Filter) (int64, []*Payment, error) { + var resp []*Payment + var total int64 + err := m.QueryNoCacheCtx(ctx, &resp, func(conn *gorm.DB, v interface{}) error { + conn = conn.Model(&Payment{}) + if req != nil { + if req.Enable != nil { + conn = conn.Where("`enable` = ?", *req.Enable) + } + if req.Mark != "" { + conn = conn.Where("`mark` = ?", req.Mark) + } + if req.Search != "" { + conn = conn.Where("`name` LIKE ?", "%"+req.Search+"%") + } + } + + return conn.Count(&total).Offset((page - 1) * size).Limit(size).Find(v).Error + }) + return total, resp, err +} diff --git a/internal/model/payment/payment.go b/internal/model/payment/payment.go new file mode 100644 index 0000000..be00208 --- /dev/null +++ b/internal/model/payment/payment.go @@ -0,0 +1,106 @@ +package payment + +import ( + "encoding/json" + "fmt" + + "gorm.io/gorm" +) + +type Payment struct { + Id int64 `gorm:"primaryKey"` + Name string `gorm:"type:varchar(100);not null;default:'';comment:Payment Name"` + Platform string `gorm:"<-:create;type:varchar(100);not null;comment:Payment Platform"` + Icon string `gorm:"type:varchar(255);default:'';comment:Payment Icon"` + Domain string `gorm:"type:varchar(255);default:'';comment:Notification Domain"` + Config string `gorm:"type:text;not null;comment:Payment Configuration"` + Description string `gorm:"type:text;comment:Payment Description"` + FeeMode uint `gorm:"type:tinyint(1);not null;default:0;comment:Fee Mode: 0: No Fee 1: Percentage 2: Fixed Amount 3: Percentage + Fixed Amount"` + FeePercent int64 `gorm:"type:int;default:0;comment:Fee Percentage"` + FeeAmount int64 `gorm:"type:int;default:0;comment:Fixed Fee Amount"` + Enable *bool `gorm:"type:tinyint(1);not null;default:0;comment:Is Enabled"` + Token string `gorm:"type:varchar(255);unique;not null;default:'';comment:Payment Token"` +} + +func (*Payment) TableName() string { + return "payment" +} + +func (l *Payment) BeforeDelete(_ *gorm.DB) (err error) { + if l.Id == -1 { + return fmt.Errorf("can't delete default payment method") + } + return nil +} + +type Filter struct { + Mark string + Enable *bool + Search string +} + +type StripeConfig struct { + PublicKey string `json:"public_key"` + SecretKey string `json:"secret_key"` + WebhookSecret string `json:"webhook_secret"` + Payment string `json:"payment"` +} + +func (l *StripeConfig) Marshal() string { + b, _ := json.Marshal(l) + return string(b) +} + +func (l *StripeConfig) Unmarshal(s string) error { + return json.Unmarshal([]byte(s), l) +} + +type AlipayF2FConfig struct { + AppId string `json:"app_id"` + PrivateKey string `json:"private_key"` + PublicKey string `json:"public_key"` + InvoiceName string `json:"invoice_name"` + Sandbox bool `json:"sandbox"` +} + +func (l *AlipayF2FConfig) Marshal() string { + b, _ := json.Marshal(l) + return string(b) +} + +func (l *AlipayF2FConfig) Unmarshal(s string) error { + return json.Unmarshal([]byte(s), l) +} + +type EPayConfig struct { + Pid string `json:"pid"` + Url string `json:"url"` + Key string `json:"key"` +} + +func (l *EPayConfig) Marshal() string { + b, _ := json.Marshal(l) + return string(b) +} + +func (l *EPayConfig) Unmarshal(s string) error { + return json.Unmarshal([]byte(s), l) +} + +type PayssionConfig struct { + PmId string `json:"pm_id"` + ApiKey string `json:"api_key"` + SecretKey string `json:"secret_key"` + Currency string `json:"currency"` + QueryUrl string `json:"query_url"` + CreateUrl string `json:"create_url"` +} + +func (l *PayssionConfig) Marshal() string { + b, _ := json.Marshal(l) + return string(b) +} + +func (l *PayssionConfig) Unmarshal(s string) error { + return json.Unmarshal([]byte(s), l) +} diff --git a/internal/model/server/default.go b/internal/model/server/default.go new file mode 100644 index 0000000..4396d85 --- /dev/null +++ b/internal/model/server/default.go @@ -0,0 +1,130 @@ +package server + +import ( + "context" + "errors" + "fmt" + + "github.com/perfect-panel/ppanel-server/internal/config" + + "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var _ Model = (*customServerModel)(nil) +var ( + cacheServerIdPrefix = "cache:server:id:" +) + +type ( + Model interface { + serverModel + customServerLogicModel + } + serverModel interface { + Insert(ctx context.Context, data *Server) error + FindOne(ctx context.Context, id int64) (*Server, error) + Update(ctx context.Context, data *Server) error + Delete(ctx context.Context, id int64) error + Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + } + + customServerModel struct { + *defaultServerModel + } + defaultServerModel struct { + cache.CachedConn + table string + } +) + +func newServerModel(db *gorm.DB, c *redis.Client) *defaultServerModel { + return &defaultServerModel{ + CachedConn: cache.NewConn(db, c), + table: "`Server`", + } +} + +// NewModel returns a model for the database table. +func NewModel(conn *gorm.DB, c *redis.Client) Model { + return &customServerModel{ + defaultServerModel: newServerModel(conn, c), + } +} + +//nolint:unused +func (m *defaultServerModel) batchGetCacheKeys(Servers ...*Server) []string { + var keys []string + for _, server := range Servers { + keys = append(keys, m.getCacheKeys(server)...) + } + return keys + +} +func (m *defaultServerModel) getCacheKeys(data *Server) []string { + if data == nil { + return []string{} + } + detailsKey := fmt.Sprintf("%s%v", CacheServerDetailPrefix, data.Id) + ServerIdKey := fmt.Sprintf("%s%v", cacheServerIdPrefix, data.Id) + configIdKey := fmt.Sprintf("%s%v", config.ServerConfigCacheKey, data.Id) + cacheKeys := []string{ + ServerIdKey, + detailsKey, + configIdKey, + } + return cacheKeys +} + +func (m *defaultServerModel) Insert(ctx context.Context, data *Server) error { + err := m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Create(&data).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultServerModel) FindOne(ctx context.Context, id int64) (*Server, error) { + ServerIdKey := fmt.Sprintf("%s%v", cacheServerIdPrefix, id) + var resp Server + err := m.QueryCtx(ctx, &resp, ServerIdKey, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Server{}).Where("`id` = ?", id).First(&resp).Error + }) + switch { + case err == nil: + return &resp, nil + default: + return nil, err + } +} + +func (m *defaultServerModel) Update(ctx context.Context, data *Server) error { + old, err := m.FindOne(ctx, data.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Save(data).Error + }, m.getCacheKeys(old)...) + return err +} + +func (m *defaultServerModel) Delete(ctx context.Context, id int64) error { + data, err := m.FindOne(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Delete(&Server{}, id).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultServerModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { + return m.TransactCtx(ctx, fn) +} diff --git a/internal/model/server/model.go b/internal/model/server/model.go new file mode 100644 index 0000000..01913fd --- /dev/null +++ b/internal/model/server/model.go @@ -0,0 +1,241 @@ +package server + +import ( + "context" + "fmt" + + "github.com/perfect-panel/ppanel-server/internal/config" + "gorm.io/gorm" +) + +type customServerLogicModel interface { + FindServerListByFilter(ctx context.Context, filter *ServerFilter) (total int64, list []*Server, err error) + ClearCache(ctx context.Context, id int64) error + QueryServerCountByServerGroups(ctx context.Context, groupIds []int64) (int64, error) + QueryAllGroup(ctx context.Context) ([]*Group, error) + BatchDeleteNodeGroup(ctx context.Context, ids []int64) error + InsertGroup(ctx context.Context, data *Group) error + FindOneGroup(ctx context.Context, id int64) (*Group, error) + UpdateGroup(ctx context.Context, data *Group) error + DeleteGroup(ctx context.Context, id int64) error + FindServerDetailByGroupIdsAndIds(ctx context.Context, groupId, ids []int64) ([]*Server, error) + FindServerListByGroupIds(ctx context.Context, groupId []int64) ([]*Server, error) + FindAllServer(ctx context.Context) ([]*Server, error) + FindNodeByServerAddrAndProtocol(ctx context.Context, serverAddr string, protocol string) ([]*Server, error) + FindServerMinSortByIds(ctx context.Context, ids []int64) (int64, error) + FindServerListByIds(ctx context.Context, ids []int64) ([]*Server, error) + InsertRuleGroup(ctx context.Context, data *RuleGroup) error + FindOneRuleGroup(ctx context.Context, id int64) (*RuleGroup, error) + UpdateRuleGroup(ctx context.Context, data *RuleGroup) error + DeleteRuleGroup(ctx context.Context, id int64) error + QueryAllRuleGroup(ctx context.Context) ([]*RuleGroup, error) +} + +var ( + CacheServerDetailPrefix = "cache:server:detail:" + cacheServerGroupAllKeys = "cache:serverGroup:all" + cacheServerRuleGroupAllKeys = "cache:serverRuleGroup:all" +) + +// ClearCache Clear Cache +func (m *customServerModel) ClearCache(ctx context.Context, id int64) error { + serverIdKey := fmt.Sprintf("%s%v", cacheServerIdPrefix, id) + configKey := fmt.Sprintf("%s%d", config.ServerConfigCacheKey, id) + + return m.DelCacheCtx(ctx, serverIdKey, configKey) +} + +// QueryServerCountByServerGroups Query Server Count By Server Groups +func (m *customServerModel) QueryServerCountByServerGroups(ctx context.Context, groupIds []int64) (int64, error) { + var count int64 + err := m.QueryNoCacheCtx(ctx, &count, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Server{}).Where("group_id IN ?", groupIds).Count(&count).Error + }) + return count, err +} + +// QueryAllGroup returns all groups. +func (m *customServerModel) QueryAllGroup(ctx context.Context) ([]*Group, error) { + var groups []*Group + err := m.QueryCtx(ctx, &groups, cacheServerGroupAllKeys, func(conn *gorm.DB, v interface{}) error { + return conn.Find(&groups).Error + }) + return groups, err +} + +// BatchDeleteNodeGroup deletes multiple groups. +func (m *customServerModel) BatchDeleteNodeGroup(ctx context.Context, ids []int64) error { + return m.Transaction(ctx, func(tx *gorm.DB) error { + for _, id := range ids { + if err := m.Delete(ctx, id); err != nil { + return err + } + } + return nil + }) +} + +// InsertGroup inserts a group. +func (m *customServerModel) InsertGroup(ctx context.Context, data *Group) error { + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Create(data).Error + }, cacheServerGroupAllKeys) +} + +// FindOneGroup finds a group. +func (m *customServerModel) FindOneGroup(ctx context.Context, id int64) (*Group, error) { + var group Group + err := m.QueryCtx(ctx, &group, fmt.Sprintf("cache:serverGroup:%v", id), func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Group{}).Where("id = ?", id).First(&group).Error + }) + return &group, err +} + +// UpdateGroup updates a group. +func (m *customServerModel) UpdateGroup(ctx context.Context, data *Group) error { + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Model(&Group{}).Where("id = ?", data.Id).Updates(data).Error + }, cacheServerGroupAllKeys, fmt.Sprintf("cache:serverGroup:%v", data.Id)) +} + +// DeleteGroup deletes a group. +func (m *customServerModel) DeleteGroup(ctx context.Context, id int64) error { + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Where("id = ?", id).Delete(&Group{}).Error + }, cacheServerGroupAllKeys, fmt.Sprintf("cache:serverGroup:%v", id)) +} + +// FindServerDetailByGroupIdsAndIds finds server details by group IDs and IDs. +func (m *customServerModel) FindServerDetailByGroupIdsAndIds(ctx context.Context, groupId, ids []int64) ([]*Server, error) { + if len(groupId) == 0 && len(ids) == 0 { + return []*Server{}, nil + } + var list []*Server + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + conn = conn. + Model(&Server{}). + Where("enable = ?", true) + if len(groupId) > 0 { + conn = conn.Where("group_id IN ?", groupId) + } + if len(ids) > 0 { + conn = conn.Where("id IN ?", ids) + } + return conn.Order("sort ASC").Find(v).Error + }) + return list, err +} + +func (m *customServerModel) FindServerListByGroupIds(ctx context.Context, groupId []int64) ([]*Server, error) { + var data []*Server + err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Server{}).Where("group_id IN ?", groupId).Find(v).Error + }) + return data, err +} + +func (m *customServerModel) FindAllServer(ctx context.Context) ([]*Server, error) { + var data []*Server + err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Server{}).Order("sort ASC").Find(v).Error + }) + return data, err +} + +func (m *customServerModel) FindNodeByServerAddrAndProtocol(ctx context.Context, serverAddr string, protocol string) ([]*Server, error) { + var data []*Server + err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Server{}).Where("server_addr = ? and protocol = ?", serverAddr, protocol).Order("sort ASC").Find(v).Error + }) + return data, err +} + +func (m *customServerModel) FindServerMinSortByIds(ctx context.Context, ids []int64) (int64, error) { + var minSort int64 + err := m.QueryNoCacheCtx(ctx, &minSort, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Server{}).Where("id IN ?", ids).Select("COALESCE(MIN(sort), 0)").Scan(v).Error + }) + return minSort, err +} + +func (m *customServerModel) FindServerListByIds(ctx context.Context, ids []int64) ([]*Server, error) { + var list []*Server + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Server{}).Where("id IN ?", ids).Find(v).Error + }) + return list, err +} + +// InsertRuleGroup inserts a group. +func (m *customServerModel) InsertRuleGroup(ctx context.Context, data *RuleGroup) error { + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Where(&RuleGroup{}).Create(data).Error + }, cacheServerRuleGroupAllKeys, fmt.Sprintf("cache:serverRuleGroup:%v", data.Id)) +} + +// FindOneRuleGroup finds a group. +func (m *customServerModel) FindOneRuleGroup(ctx context.Context, id int64) (*RuleGroup, error) { + var group RuleGroup + err := m.QueryCtx(ctx, &group, fmt.Sprintf("cache:serverRuleGroup:%v", id), func(conn *gorm.DB, v interface{}) error { + return conn.Where(&RuleGroup{}).Model(&RuleGroup{}).Where("id = ?", id).First(&group).Error + }) + return &group, err +} + +// UpdateRuleGroup updates a group. +func (m *customServerModel) UpdateRuleGroup(ctx context.Context, data *RuleGroup) error { + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Where(&RuleGroup{}).Model(&RuleGroup{}).Where("id = ?", data.Id).Save(data).Error + }, cacheServerRuleGroupAllKeys, fmt.Sprintf("cache:serverRuleGroup:%v", data.Id)) +} + +// DeleteRuleGroup deletes a group. +func (m *customServerModel) DeleteRuleGroup(ctx context.Context, id int64) error { + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Where(&RuleGroup{}).Where("id = ?", id).Delete(&RuleGroup{}).Error + }, cacheServerRuleGroupAllKeys, fmt.Sprintf("cache:serverRuleGroup:%v", id)) +} + +// QueryAllRuleGroup returns all rule groups. +func (m *customServerModel) QueryAllRuleGroup(ctx context.Context) ([]*RuleGroup, error) { + var groups []*RuleGroup + err := m.QueryCtx(ctx, &groups, cacheServerRuleGroupAllKeys, func(conn *gorm.DB, v interface{}) error { + return conn.Where(&RuleGroup{}).Find(&groups).Error + }) + return groups, err +} + +func (m *customServerModel) FindServerListByFilter(ctx context.Context, filter *ServerFilter) (total int64, list []*Server, err error) { + var data []*Server + if filter == nil { + filter = &ServerFilter{ + Page: 1, + Size: 10, + } + } + + if filter.Page <= 0 { + filter.Page = 1 + } + if filter.Size <= 0 { + filter.Size = 10 + } + + err = m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { + query := conn.Model(&Server{}).Order("sort ASC") + if filter.Group > 0 { + query = conn.Where("group_id = ?", filter.Group) + } + if filter.Search != "" { + query = query.Where("name LIKE ? OR server_addr LIKE ?", "%"+filter.Search+"%", "%"+filter.Search+"%") + } + if filter.Tag != "" { + query = query.Where("tag LIKE ?", "%"+filter.Tag+"%") + } + return query.Count(&total).Limit(filter.Size).Offset((filter.Page - 1) * filter.Size).Find(v).Error + }) + if err != nil { + return 0, nil, err + } + return total, data, nil +} diff --git a/internal/model/server/server.go b/internal/model/server/server.go new file mode 100644 index 0000000..3dbbee5 --- /dev/null +++ b/internal/model/server/server.go @@ -0,0 +1,210 @@ +package server + +import ( + "time" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "gorm.io/gorm" +) + +const ( + RelayModeNone = "none" + RelayModeAll = "all" + RelayModeRandom = "random" +) + +type ServerFilter struct { + Id int64 + Tag string + Group int64 + Search string + Page int + Size int +} + +type Server struct { + Id int64 `gorm:"primary_key"` + Name string `gorm:"type:varchar(100);not null;default:'';comment:Node Name"` + Tags string `gorm:"type:varchar(128);not null;default:'';comment:Tags"` + Country string `gorm:"type:varchar(128);not null;default:'';comment:Country"` + City string `gorm:"type:varchar(128);not null;default:'';comment:City"` + Latitude string `gorm:"type:varchar(128);not null;default:'';comment:Latitude"` + Longitude string `gorm:"type:varchar(128);not null;default:'';comment:Longitude"` + ServerAddr string `gorm:"type:varchar(100);not null;default:'';comment:Server Address"` + RelayMode string `gorm:"type:varchar(20);not null;default:'none';comment:Relay Mode"` + RelayNode string `gorm:"type:text;comment:Relay Node"` + SpeedLimit int `gorm:"type:int;not null;default:0;comment:Speed Limit"` + TrafficRatio float32 `gorm:"type:DECIMAL(4,2);not null;default:0;comment:Traffic Ratio"` + GroupId int64 `gorm:"index:idx_group_id;type:int;default:null;comment:Group ID"` + Protocol string `gorm:"type:varchar(20);not null;default:'';comment:Protocol"` + Config string `gorm:"type:text;comment:Config"` + Enable *bool `gorm:"type:tinyint(1);not null;default:1;comment:Enabled"` + Sort int64 `gorm:"type:int;not null;default:0;comment:Sort"` + LastReportedAt time.Time `gorm:"comment:Last Reported Time"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (*Server) TableName() string { + return "server" +} + +func (s *Server) BeforeDelete(tx *gorm.DB) error { + logger.Debugf("[Server] BeforeDelete") + + if err := tx.Exec("UPDATE `server` SET sort = sort - 1 WHERE sort > ?", s.Sort).Error; err != nil { + return err + } + // 删除后重新排序,防止因 sort 缺口导致问题 + if err := reorderSort(tx); err != nil { + return err + } + + return nil +} + +func (s *Server) BeforeUpdate(tx *gorm.DB) error { + logger.Debugf("[Server] BeforeUpdate") + + var count int64 + if err := tx.Model(&Server{}).Where("sort = ? AND id != ?", s.Sort, s.Id).Count(&count).Error; err != nil { + return err + } + + if count > 0 { + logger.Debugf("[Server] Duplicate sort found, reordering...") + if err := reorderSort(tx); err != nil { + return err + } + } + + return nil +} + +func (s *Server) BeforeCreate(tx *gorm.DB) error { + logger.Debugf("[Server] BeforeCreate") + if s.Sort == 0 { + var maxSort int64 + if err := tx.Model(&Server{}).Select("COALESCE(MAX(sort), 0)").Scan(&maxSort).Error; err != nil { + return err + } + s.Sort = maxSort + 1 + } + return nil +} + +type Vless struct { + Port int `json:"port"` + Flow string `json:"flow"` + Transport string `json:"transport"` + TransportConfig TransportConfig `json:"transport_config"` + Security string `json:"security"` + SecurityConfig SecurityConfig `json:"security_config"` +} + +type Vmess struct { + Port int `json:"port"` + Flow string `json:"flow"` + Transport string `json:"transport"` + TransportConfig TransportConfig `json:"transport_config"` + Security string `json:"security"` + SecurityConfig SecurityConfig `json:"security_config"` +} + +type Trojan struct { + Port int `json:"port"` + Flow string `json:"flow"` + Transport string `json:"transport"` + TransportConfig TransportConfig `json:"transport_config"` + Security string `json:"security"` + SecurityConfig SecurityConfig `json:"security_config"` +} + +type Shadowsocks struct { + Method string `json:"method"` + Port int `json:"port"` + ServerKey string `json:"server_key"` +} + +type Hysteria2 struct { + Port int `json:"port"` + HopPorts string `json:"hop_ports"` + HopInterval int `json:"hop_interval"` + ObfsPassword string `json:"obfs_password"` + SecurityConfig SecurityConfig `json:"security_config"` +} + +type Tuic struct { + Port int `json:"port"` + SecurityConfig SecurityConfig `json:"security_config"` +} + +type TransportConfig struct { + Path string `json:"path,omitempty"` // ws/httpupgrade + Host string `json:"host,omitempty"` + ServiceName string `json:"service_name"` // grpc +} + +type SecurityConfig struct { + SNI string `json:"sni"` + AllowInsecure bool `json:"allow_insecure"` + Fingerprint string `json:"fingerprint"` + RealityServerAddr string `json:"reality_server_addr"` + RealityServerPort int `json:"reality_server_port"` + RealityPrivateKey string `json:"reality_private_key"` + RealityPublicKey string `json:"reality_public_key"` + RealityShortId string `json:"reality_short_id"` +} + +type NodeRelay struct { + Host string `json:"host"` + Port int `json:"port"` + Prefix string `json:"prefix"` +} + +type Group struct { + Id int64 `gorm:"primary_key"` + Name string `gorm:"type:varchar(100);not null;default:'';comment:Group Name"` + Description string `gorm:"type:varchar(255);default:'';comment:Group Description"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (Group) TableName() string { + return "server_group" +} + +type RuleGroup struct { + Id int64 `gorm:"primary_key"` + Icon string `gorm:"type:MEDIUMTEXT;comment:Rule Group Icon"` + Name string `gorm:"type:varchar(100);not null;default:'';comment:Rule Group Name"` + Tags string `gorm:"type:text;comment:Selected Node Tags"` + Rules string `gorm:"type:MEDIUMTEXT;comment:Rules"` + Enable bool `gorm:"type:tinyint(1);not null;default:1;comment:Rule Group Enable"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (RuleGroup) TableName() string { + return "server_rule_group" +} + +func reorderSort(tx *gorm.DB) error { + var servers []*Server + if err := tx.Model(&Server{}).Order("sort ASC").Find(&servers).Error; err != nil { + return err + } + + for i, server := range servers { + newSort := int64(i + 1) + if server.Sort != newSort { + if err := tx.Model(&Server{}). + Where("id = ?", server.Id). + Update("sort", newSort).Error; err != nil { + return err + } + } + } + return nil +} diff --git a/internal/model/subscribe/default.go b/internal/model/subscribe/default.go new file mode 100644 index 0000000..6832026 --- /dev/null +++ b/internal/model/subscribe/default.go @@ -0,0 +1,126 @@ +package subscribe + +import ( + "context" + "errors" + "fmt" + + "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var _ Model = (*customSubscribeModel)(nil) +var ( + cacheSubscribeIdPrefix = "cache:subscribe:id:" +) + +type ( + Model interface { + subscribeModel + customSubscribeLogicModel + } + subscribeModel interface { + Insert(ctx context.Context, data *Subscribe, tx ...*gorm.DB) error + FindOne(ctx context.Context, id int64) (*Subscribe, error) + Update(ctx context.Context, data *Subscribe, tx ...*gorm.DB) error + Delete(ctx context.Context, id int64, tx ...*gorm.DB) error + Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + } + + customSubscribeModel struct { + *defaultSubscribeModel + } + defaultSubscribeModel struct { + cache.CachedConn + table string + } +) + +func newSubscribeModel(db *gorm.DB, c *redis.Client) *defaultSubscribeModel { + return &defaultSubscribeModel{ + CachedConn: cache.NewConn(db, c), + table: "`subscribe`", + } +} + +//nolint:unused +func (m *defaultSubscribeModel) batchGetCacheKeys(Subscribes ...*Subscribe) []string { + var keys []string + for _, subscribe := range Subscribes { + keys = append(keys, m.getCacheKeys(subscribe)...) + } + return keys + +} +func (m *defaultSubscribeModel) getCacheKeys(data *Subscribe) []string { + if data == nil { + return []string{} + } + SubscribeIdKey := fmt.Sprintf("%s%v", cacheSubscribeIdPrefix, data.Id) + cacheKeys := []string{ + SubscribeIdKey, + } + return cacheKeys +} + +func (m *defaultSubscribeModel) Insert(ctx context.Context, data *Subscribe, tx ...*gorm.DB) error { + err := m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Create(&data).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultSubscribeModel) FindOne(ctx context.Context, id int64) (*Subscribe, error) { + SubscribeIdKey := fmt.Sprintf("%s%v", cacheSubscribeIdPrefix, id) + var resp Subscribe + err := m.QueryCtx(ctx, &resp, SubscribeIdKey, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Subscribe{}).Where("`id` = ?", id).First(&resp).Error + }) + switch { + case err == nil: + return &resp, nil + default: + return nil, err + } +} + +func (m *defaultSubscribeModel) Update(ctx context.Context, data *Subscribe, tx ...*gorm.DB) error { + old, err := m.FindOne(ctx, data.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + if len(tx) > 0 { + db = tx[0] + } + return db.Save(data).Error + }, m.getCacheKeys(old)...) + return err +} + +func (m *defaultSubscribeModel) Delete(ctx context.Context, id int64, tx ...*gorm.DB) error { + data, err := m.FindOne(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + if len(tx) > 0 { + db = tx[0] + } + return db.Delete(&Subscribe{}, id).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultSubscribeModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { + return m.TransactCtx(ctx, fn) +} diff --git a/internal/model/subscribe/model.go b/internal/model/subscribe/model.go new file mode 100644 index 0000000..f3b1142 --- /dev/null +++ b/internal/model/subscribe/model.go @@ -0,0 +1,109 @@ +package subscribe + +import ( + "context" + + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +// type Details struct { +// Id int64 `gorm:"primaryKey"` +// Name string `gorm:"type:varchar(255);not null;default:'';comment:Subscribe Name"` +// Description string `gorm:"type:text;comment:Subscribe Description"` +// UnitPrice int64 `gorm:"type:int;not null;default:0;comment:Unit Price"` +// UnitTime string `gorm:"type:varchar(255);not null;default:'';comment:Unit Time"` +// Discount string `gorm:"type:text;comment:Discount"` +// Replacement int64 `gorm:"type:int;not null;default:0;comment:Replacement"` +// Inventory int64 `gorm:"type:int;not null;default:0;comment:Inventory"` +// Traffic int64 `gorm:"type:int;not null;default:0;comment:Traffic"` +// SpeedLimit int64 `gorm:"type:int;not null;default:0;comment:Speed Limit"` +// DeviceLimit int64 `gorm:"type:int;not null;default:0;comment:Device Limit"` +// GroupId int64 `gorm:"type:bigint;comment:Group Id"` +// Quota int64 `gorm:"type:int;not null;default:0;comment:Quota"` +// Show *bool `gorm:"type:tinyint(1);not null;default:0;comment:Show"` +// Sell *bool `gorm:"type:tinyint(1);not null;default:0;comment:Sell"` +// DeductionRatio int64 `gorm:"type:int;default:0;comment:Deduction Ratio"` +// PurchaseWithDiscount bool `gorm:"type:tinyint(1);default:0;comment:PurchaseWithDiscount"` +// ResetCycle int64 `gorm:"type:int;default:0;comment:Reset Cycle"` +// RenewalReset bool `gorm:"type:tinyint(1);default:0;comment:Renew Reset"` +// } +type customSubscribeLogicModel interface { + QuerySubscribeListByPage(ctx context.Context, page, size int, group int64, search string) (total int64, list []*Subscribe, err error) + QuerySubscribeList(ctx context.Context) ([]*Subscribe, error) + QuerySubscribeListByShow(ctx context.Context) ([]*Subscribe, error) + QuerySubscribeIdsByServerIdAndServerGroupId(ctx context.Context, serverId, serverGroupId int64) ([]*Subscribe, error) + QuerySubscribeMinSortByIds(ctx context.Context, ids []int64) (int64, error) + QuerySubscribeListByIds(ctx context.Context, ids []int64) ([]*Subscribe, error) +} + +// NewModel returns a model for the database table. +func NewModel(conn *gorm.DB, c *redis.Client) Model { + return &customSubscribeModel{ + defaultSubscribeModel: newSubscribeModel(conn, c), + } +} + +// QuerySubscribeListByPage Get Subscribe List +func (m *customSubscribeModel) QuerySubscribeListByPage(ctx context.Context, page, size int, group int64, search string) (total int64, list []*Subscribe, err error) { + err = m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + // About to be abandoned + _ = conn.Model(&Subscribe{}). + Where("sort = ?", 0). + Update("sort", gorm.Expr("id")) + + conn = conn.Model(&Subscribe{}) + if group > 0 { + conn = conn.Where("group_id = ?", group) + } + if search != "" { + conn = conn.Where("`name` like ? or `description` like ?", "%"+search+"%", "%"+search+"%") + } + return conn.Count(&total).Order("sort ASC").Limit(size).Offset((page - 1) * size).Find(v).Error + }) + return total, list, err +} + +// QuerySubscribeList Get Subscribe List +func (m *customSubscribeModel) QuerySubscribeList(ctx context.Context) ([]*Subscribe, error) { + var list []*Subscribe + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + conn = conn.Model(&Subscribe{}) + return conn.Where("`sell` = true").Order("sort ").Find(v).Error + }) + return list, err +} + +func (m *customSubscribeModel) QuerySubscribeIdsByServerIdAndServerGroupId(ctx context.Context, serverId, serverGroupId int64) ([]*Subscribe, error) { + var data []*Subscribe + err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Subscribe{}).Where("FIND_IN_SET(?, server)", serverId).Or("FIND_IN_SET(?, server_group)", serverGroupId).Find(v).Error + }) + return data, err +} + +// QuerySubscribeListByShow Get Subscribe List By Show +func (m *customSubscribeModel) QuerySubscribeListByShow(ctx context.Context) ([]*Subscribe, error) { + var list []*Subscribe + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + conn = conn.Model(&Subscribe{}) + return conn.Where("`show` = true").Find(v).Error + }) + return list, err +} + +func (m *customSubscribeModel) QuerySubscribeMinSortByIds(ctx context.Context, ids []int64) (int64, error) { + var minSort int64 + err := m.QueryNoCacheCtx(ctx, &minSort, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Subscribe{}).Where("id IN ?", ids).Select("COALESCE(MIN(sort), 0)").Scan(v).Error + }) + return minSort, err +} + +func (m *customSubscribeModel) QuerySubscribeListByIds(ctx context.Context, ids []int64) ([]*Subscribe, error) { + var list []*Subscribe + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Subscribe{}).Where("id IN ?", ids).Find(v).Error + }) + return list, err +} diff --git a/internal/model/subscribe/subscribe.go b/internal/model/subscribe/subscribe.go new file mode 100644 index 0000000..2c705f8 --- /dev/null +++ b/internal/model/subscribe/subscribe.go @@ -0,0 +1,66 @@ +package subscribe + +import ( + "time" + + "gorm.io/gorm" +) + +type Subscribe struct { + Id int64 `gorm:"primaryKey"` + Name string `gorm:"type:varchar(255);not null;default:'';comment:Subscribe Name"` + Description string `gorm:"type:text;comment:Subscribe Description"` + UnitPrice int64 `gorm:"type:int;not null;default:0;comment:Unit Price"` + UnitTime string `gorm:"type:varchar(255);not null;default:'';comment:Unit Time"` + Discount string `gorm:"type:text;comment:Discount"` + Replacement int64 `gorm:"type:int;not null;default:0;comment:Replacement"` + Inventory int64 `gorm:"type:int;not null;default:0;comment:Inventory"` + Traffic int64 `gorm:"type:int;not null;default:0;comment:Traffic"` + SpeedLimit int64 `gorm:"type:int;not null;default:0;comment:Speed Limit"` + DeviceLimit int64 `gorm:"type:int;not null;default:0;comment:Device Limit"` + Quota int64 `gorm:"type:int;not null;default:0;comment:Quota"` + GroupId int64 `gorm:"type:bigint;comment:Group Id"` + ServerGroup string `gorm:"type:varchar(255);comment:Server Group"` + Server string `gorm:"type:varchar(255);comment:Server"` + Show *bool `gorm:"type:tinyint(1);not null;default:0;comment:Show portal page"` + Sell *bool `gorm:"type:tinyint(1);not null;default:0;comment:Sell"` + Sort int64 `gorm:"type:int;not null;default:0;comment:Sort"` + DeductionRatio int64 `gorm:"type:int;default:0;comment:Deduction Ratio"` + AllowDeduction *bool `gorm:"type:tinyint(1);default:1;comment:Allow deduction"` + ResetCycle int64 `gorm:"type:int;default:0;comment:Reset Cycle: 0: No Reset, 1: 1st, 2: Monthly, 3: Yearly"` + RenewalReset *bool `gorm:"type:tinyint(1);default:0;comment:Renew Reset"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (*Subscribe) TableName() string { + return "subscribe" +} + +func (s *Subscribe) BeforeCreate(tx *gorm.DB) error { + if s.Sort == 0 { + var maxSort int64 + if err := tx.Model(&Subscribe{}).Select("COALESCE(MAX(sort), 0)").Scan(&maxSort).Error; err != nil { + return err + } + s.Sort = maxSort + 1 + } + return nil +} + +type Discount struct { + Months int64 `json:"months"` + Discount int64 `json:"discount"` +} + +type Group struct { + Id int64 `gorm:"primaryKey"` + Name string `gorm:"type:varchar(255);not null;default:'';comment:Group Name"` + Description string `gorm:"type:text;comment:Group Description"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (Group) TableName() string { + return "subscribe_group" +} diff --git a/internal/model/subscribeType/default.go b/internal/model/subscribeType/default.go new file mode 100644 index 0000000..788f8f2 --- /dev/null +++ b/internal/model/subscribeType/default.go @@ -0,0 +1,117 @@ +package subscribeType + +import ( + "context" + "errors" + "fmt" + + "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var _ Model = (*customSubscribeTypeModel)(nil) +var ( + cacheSubscribeTypeIdPrefix = "cache:subscribeType:id:" +) + +type ( + Model interface { + subscribeTypeModel + customSubscribeTypeLogicModel + } + subscribeTypeModel interface { + Insert(ctx context.Context, data *SubscribeType) error + FindOne(ctx context.Context, id int64) (*SubscribeType, error) + Update(ctx context.Context, data *SubscribeType) error + Delete(ctx context.Context, id int64) error + Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + } + + customSubscribeTypeModel struct { + *defaultSubscribeTypeModel + } + defaultSubscribeTypeModel struct { + cache.CachedConn + table string + } +) + +func newSubscribeTypeModel(db *gorm.DB, c *redis.Client) *defaultSubscribeTypeModel { + return &defaultSubscribeTypeModel{ + CachedConn: cache.NewConn(db, c), + table: "`SubscribeType`", + } +} + +//nolint:unused +func (m *defaultSubscribeTypeModel) batchGetCacheKeys(SubscribeTypes ...*SubscribeType) []string { + var keys []string + for _, subscribeType := range SubscribeTypes { + keys = append(keys, m.getCacheKeys(subscribeType)...) + } + return keys + +} +func (m *defaultSubscribeTypeModel) getCacheKeys(data *SubscribeType) []string { + if data == nil { + return []string{} + } + SubscribeTypeIdKey := fmt.Sprintf("%s%v", cacheSubscribeTypeIdPrefix, data.Id) + cacheKeys := []string{ + SubscribeTypeIdKey, + } + return cacheKeys +} + +func (m *defaultSubscribeTypeModel) Insert(ctx context.Context, data *SubscribeType) error { + err := m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Create(&data).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultSubscribeTypeModel) FindOne(ctx context.Context, id int64) (*SubscribeType, error) { + SubscribeTypeIdKey := fmt.Sprintf("%s%v", cacheSubscribeTypeIdPrefix, id) + var resp SubscribeType + err := m.QueryCtx(ctx, &resp, SubscribeTypeIdKey, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&SubscribeType{}).Where("`id` = ?", id).First(&resp).Error + }) + switch { + case err == nil: + return &resp, nil + default: + return nil, err + } +} + +func (m *defaultSubscribeTypeModel) Update(ctx context.Context, data *SubscribeType) error { + old, err := m.FindOne(ctx, data.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Save(data).Error + }, m.getCacheKeys(old)...) + return err +} + +func (m *defaultSubscribeTypeModel) Delete(ctx context.Context, id int64) error { + data, err := m.FindOne(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Delete(&SubscribeType{}, id).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultSubscribeTypeModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { + return m.TransactCtx(ctx, fn) +} diff --git a/internal/model/subscribeType/model.go b/internal/model/subscribeType/model.go new file mode 100644 index 0000000..52e7e0f --- /dev/null +++ b/internal/model/subscribeType/model.go @@ -0,0 +1,16 @@ +package subscribeType + +import ( + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +type customSubscribeTypeLogicModel interface { +} + +// NewModel returns a model for the database table. +func NewModel(conn *gorm.DB, c *redis.Client) Model { + return &customSubscribeTypeModel{ + defaultSubscribeTypeModel: newSubscribeTypeModel(conn, c), + } +} diff --git a/internal/model/subscribeType/subscribeType.go b/internal/model/subscribeType/subscribeType.go new file mode 100644 index 0000000..c0a9d94 --- /dev/null +++ b/internal/model/subscribeType/subscribeType.go @@ -0,0 +1,15 @@ +package subscribeType + +import "time" + +type SubscribeType struct { + Id int64 `gorm:"primary_key"` + Name string `gorm:"type:varchar(50);default:'';not null;comment:订阅类型"` + Mark string `gorm:"type:varchar(255);default:'';not null;comment:订阅标识"` + CreatedAt time.Time `gorm:"<-:create;comment:创建时间"` + UpdatedAt time.Time `gorm:"comment:更新时间"` +} + +func (SubscribeType) TableName() string { + return "subscribe_type" +} diff --git a/internal/model/system/default.go b/internal/model/system/default.go new file mode 100644 index 0000000..09f53fc --- /dev/null +++ b/internal/model/system/default.go @@ -0,0 +1,119 @@ +package system + +import ( + "context" + "errors" + "fmt" + + "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var ( + cacheSystemIdPrefix = "cache:System:id:" + cacheSystemKeyPrefix = "cache:System:key:" +) +var _ Model = (*customSystemModel)(nil) + +type ( + Model interface { + systemModel + customSystemLogicModel + } + systemModel interface { + Insert(ctx context.Context, data *System) error + FindOne(ctx context.Context, id int64) (*System, error) + FindOneByKey(ctx context.Context, email string) (*System, error) + Update(ctx context.Context, data *System) error + Delete(ctx context.Context, id int64) error + Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + } + + customSystemModel struct { + *defaultSystemModel + } + defaultSystemModel struct { + cache.CachedConn + table string + } +) + +func newSystemModel(db *gorm.DB, c *redis.Client) *defaultSystemModel { + return &defaultSystemModel{ + CachedConn: cache.NewConn(db, c), + table: "`System`", + } +} + +func (m *defaultSystemModel) getCacheKeys(data *System) []string { + if data == nil { + return []string{} + } + SystemIdKey := fmt.Sprintf("%s%v", cacheSystemIdPrefix, data.Id) + cacheKeys := []string{ + SystemIdKey, + } + return cacheKeys +} + +func (m *defaultSystemModel) FindOneByKey(ctx context.Context, key string) (*System, error) { + system := new(System) + cacheKey := fmt.Sprintf("%s%v", cacheSystemKeyPrefix, key) + err := m.QueryCtx(ctx, system, cacheKey, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&System{}).Where("`key` = ?", key).First(v).Error + }) + return system, err +} + +func (m *defaultSystemModel) Insert(ctx context.Context, data *System) error { + err := m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Create(&data).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultSystemModel) FindOne(ctx context.Context, id int64) (*System, error) { + SystemIdKey := fmt.Sprintf("%s%v", cacheSystemIdPrefix, id) + var resp System + err := m.QueryCtx(ctx, &resp, SystemIdKey, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&System{}).Where("`id` = ?", id).First(&resp).Error + }) + switch { + case err == nil: + return &resp, nil + default: + return nil, err + } +} + +func (m *defaultSystemModel) Update(ctx context.Context, data *System) error { + old, err := m.FindOne(ctx, data.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Save(data).Error + }, m.getCacheKeys(old)...) + return err +} + +func (m *defaultSystemModel) Delete(ctx context.Context, id int64) error { + data, err := m.FindOne(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Delete(&System{}, id).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultSystemModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { + return m.TransactCtx(ctx, fn) +} diff --git a/internal/model/system/model.go b/internal/model/system/model.go new file mode 100644 index 0000000..1439431 --- /dev/null +++ b/internal/model/system/model.go @@ -0,0 +1,154 @@ +package system + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +type customSystemLogicModel interface { + GetSmsConfig(ctx context.Context) ([]*System, error) + GetSiteConfig(ctx context.Context) ([]*System, error) + GetSubscribeConfig(ctx context.Context) ([]*System, error) + GetRegisterConfig(ctx context.Context) ([]*System, error) + GetVerifyConfig(ctx context.Context) ([]*System, error) + GetNodeConfig(ctx context.Context) ([]*System, error) + GetInviteConfig(ctx context.Context) ([]*System, error) + GetTosConfig(ctx context.Context) ([]*System, error) + GetCurrencyConfig(ctx context.Context) ([]*System, error) + GetVerifyCodeConfig(ctx context.Context) ([]*System, error) + UpdateNodeMultiplierConfig(ctx context.Context, config string) error + FindNodeMultiplierConfig(ctx context.Context) (*System, error) +} + +// NewModel returns a model for the database table. +func NewModel(conn *gorm.DB, c *redis.Client) Model { + return &customSystemModel{ + defaultSystemModel: newSystemModel(conn, c), + } +} + +// GetSmsConfig returns the sms config. +func (m *customSystemModel) GetSmsConfig(ctx context.Context) ([]*System, error) { + var configs []*System + err := m.QueryCtx(ctx, &configs, config.SmsConfigKey, func(conn *gorm.DB, v interface{}) error { + return conn.Where("`category` = ?", "sms").Find(v).Error + }) + return configs, err +} + +// GetSiteConfig returns the site config. +func (m *customSystemModel) GetSiteConfig(ctx context.Context) ([]*System, error) { + var configs []*System + err := m.QueryCtx(ctx, &configs, config.SiteConfigKey, func(conn *gorm.DB, v interface{}) error { + return conn.Where("`category` = ?", "site").Find(v).Error + }) + return configs, err +} + +// GetEmailConfig returns the email config. +func (m *customSystemModel) GetEmailConfig(ctx context.Context) ([]*System, error) { + var configs []*System + err := m.QueryCtx(ctx, &configs, config.EmailSmtpConfigKey, func(conn *gorm.DB, v interface{}) error { + return conn.Where("`category` = ?", "email").Find(v).Error + }) + return configs, err +} + +// GetSubscribeConfig returns the subscribe config. +func (m *customSystemModel) GetSubscribeConfig(ctx context.Context) ([]*System, error) { + var configs []*System + err := m.QueryCtx(ctx, &configs, config.SubscribeConfigKey, func(conn *gorm.DB, v interface{}) error { + return conn.Where("`category` = ?", "subscribe").Find(v).Error + }) + return configs, err +} + +// GetRegisterConfig returns the register config. +func (m *customSystemModel) GetRegisterConfig(ctx context.Context) ([]*System, error) { + var configs []*System + err := m.QueryCtx(ctx, &configs, config.RegisterConfigKey, func(conn *gorm.DB, v interface{}) error { + return conn.Where("`category` = ?", "register").Find(v).Error + }) + return configs, err +} + +// GetVerifyConfig returns the verify config. +func (m *customSystemModel) GetVerifyConfig(ctx context.Context) ([]*System, error) { + var configs []*System + err := m.QueryCtx(ctx, &configs, config.VerifyConfigKey, func(conn *gorm.DB, v interface{}) error { + return conn.Where("`category` = ?", "verify").Find(v).Error + }) + return configs, err +} + +// GetNodeConfig returns the server config. +func (m *customSystemModel) GetNodeConfig(ctx context.Context) ([]*System, error) { + var configs []*System + err := m.QueryCtx(ctx, &configs, config.NodeConfigKey, func(conn *gorm.DB, v interface{}) error { + return conn.Where("`category` = ?", "server").Find(v).Error + }) + return configs, err +} + +// GetInviteConfig returns the invite config. +func (m *customSystemModel) GetInviteConfig(ctx context.Context) ([]*System, error) { + var configs []*System + err := m.QueryCtx(ctx, &configs, config.InviteConfigKey, func(conn *gorm.DB, v interface{}) error { + return conn.Where("`category` = ?", "invite").Find(v).Error + }) + return configs, err +} + +// GetTelegramConfig returns the telegram config. +func (m *customSystemModel) GetTelegramConfig(ctx context.Context) ([]*System, error) { + var configs []*System + err := m.QueryCtx(ctx, &configs, config.TelegramConfigKey, func(conn *gorm.DB, v interface{}) error { + return conn.Where("`category` = ?", "telegram").Find(v).Error + }) + return configs, err +} + +// GetTosConfig returns the tos config. +func (m *customSystemModel) GetTosConfig(ctx context.Context) ([]*System, error) { + var configs []*System + err := m.QueryCtx(ctx, &configs, config.TosConfigKey, func(conn *gorm.DB, v interface{}) error { + return conn.Where("`category` = ?", "tos").Find(v).Error + }) + return configs, err +} + +// GetCurrencyConfig returns the currency config. +func (m *customSystemModel) GetCurrencyConfig(ctx context.Context) ([]*System, error) { + var configs []*System + err := m.QueryCtx(ctx, &configs, config.CurrencyConfigKey, func(conn *gorm.DB, v interface{}) error { + return conn.Where("`category` = ?", "currency").Find(v).Error + }) + return configs, err +} + +func (m *customSystemModel) UpdateNodeMultiplierConfig(ctx context.Context, config string) error { + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { + return conn.Model(&System{}).Where("`category` = ? AND `key` = ?", "server", "NodeMultiplierConfig").Update("value", config).Error + }) +} + +func (m *customSystemModel) FindNodeMultiplierConfig(ctx context.Context) (*System, error) { + var data System + err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { + return conn.Where("`category` = ? AND `key` = ?", "server", "NodeMultiplierConfig").Find(v).Error + }) + return &data, err +} + +// GetVerifyCodeConfig returns the verify code config. + +func (m *customSystemModel) GetVerifyCodeConfig(ctx context.Context) ([]*System, error) { + var configs []*System + err := m.QueryCtx(ctx, &configs, config.VerifyCodeConfigKey, func(conn *gorm.DB, v interface{}) error { + return conn.Where("`category` = ?", "verify_code").Find(v).Error + }) + return configs, err +} diff --git a/internal/model/system/system.go b/internal/model/system/system.go new file mode 100644 index 0000000..23b8438 --- /dev/null +++ b/internal/model/system/system.go @@ -0,0 +1,18 @@ +package system + +import "time" + +type System struct { + Id int64 `gorm:"primarykey"` + Category string `gorm:"type:varchar(100);default:'';not null;comment:Category"` + Key string `gorm:"index:index_key;unique;type:varchar(100);default:'';not null;comment:Key Name"` + Value string `gorm:"type:text;not null;comment:Key Value"` + Type string `gorm:"type:varchar(50);default:'';not null;comment:Type"` + Desc string `gorm:"type:text;not null;comment:Description"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (System) TableName() string { + return "system" +} diff --git a/internal/model/ticket/default.go b/internal/model/ticket/default.go new file mode 100644 index 0000000..d52a6df --- /dev/null +++ b/internal/model/ticket/default.go @@ -0,0 +1,118 @@ +package ticket + +import ( + "context" + "errors" + "fmt" + + "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var _ Model = (*customTicketModel)(nil) +var ( + cacheTicketIdPrefix = "cache:ticket:id:" +) + +type ( + Model interface { + ticketModel + customTicketLogicModel + } + ticketModel interface { + Insert(ctx context.Context, data *Ticket) error + FindOne(ctx context.Context, id int64) (*Ticket, error) + Update(ctx context.Context, data *Ticket) error + Delete(ctx context.Context, id int64) error + Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + } + + customTicketModel struct { + *defaultTicketModel + } + defaultTicketModel struct { + cache.CachedConn + table string + } +) + +func newTicketModel(db *gorm.DB, c *redis.Client) *defaultTicketModel { + return &defaultTicketModel{ + CachedConn: cache.NewConn(db, c), + table: "`ticket`", + } +} + +//nolint:unused +func (m *defaultTicketModel) batchGetCacheKeys(Tickets ...*Ticket) []string { + var keys []string + for _, ticket := range Tickets { + keys = append(keys, m.getCacheKeys(ticket)...) + } + return keys + +} +func (m *defaultTicketModel) getCacheKeys(data *Ticket) []string { + if data == nil { + return []string{} + } + ticketIdKey := fmt.Sprintf("%s%v", cacheTicketIdPrefix, data.Id) + cacheKeys := []string{ + ticketIdKey, + } + return cacheKeys +} + +func (m *defaultTicketModel) Insert(ctx context.Context, data *Ticket) error { + err := m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Create(&data).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultTicketModel) FindOne(ctx context.Context, id int64) (*Ticket, error) { + TicketIdKey := fmt.Sprintf("%s%v", cacheTicketIdPrefix, id) + var resp Ticket + err := m.QueryCtx(ctx, &resp, TicketIdKey, func(conn *gorm.DB, v interface{}) error { + + return conn.Model(&Ticket{}).Where("`id` = ?", id).First(&resp).Error + }) + switch { + case err == nil: + return &resp, nil + default: + return nil, err + } +} + +func (m *defaultTicketModel) Update(ctx context.Context, data *Ticket) error { + old, err := m.FindOne(ctx, data.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Save(data).Error + }, m.getCacheKeys(old)...) + return err +} + +func (m *defaultTicketModel) Delete(ctx context.Context, id int64) error { + data, err := m.FindOne(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Delete(&Ticket{}, id).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultTicketModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { + return m.TransactCtx(ctx, fn) +} diff --git a/internal/model/ticket/model.go b/internal/model/ticket/model.go new file mode 100644 index 0000000..0662aa7 --- /dev/null +++ b/internal/model/ticket/model.go @@ -0,0 +1,98 @@ +package ticket + +import ( + "context" + "fmt" + "time" + + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var cacheTicketDetailPrefix = "cache:ticket:detail:" + +type Details struct { + Id int64 `gorm:"primaryKey"` + Title string `gorm:"type:varchar(255);not null;default:'';comment:Title"` + Description string `gorm:"type:text;comment:Description"` + UserId int64 `gorm:"type:bigint;not null;default:0;comment:UserId"` + Status uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Status"` + Follows []Follow `gorm:"foreignKey:TicketId;references:Id"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} +type customTicketLogicModel interface { + QueryTicketDetail(ctx context.Context, id int64) (*Details, error) + InsertTicketFollow(ctx context.Context, data *Follow) error + QueryTicketList(ctx context.Context, page, size int, userId int64, status *uint8, search string) (int64, []*Ticket, error) + UpdateTicketStatus(ctx context.Context, id, userId int64, status uint8) error + QueryWaitReplyTotal(ctx context.Context) (int64, error) +} + +// NewModel returns a model for the database table. +func NewModel(conn *gorm.DB, c *redis.Client) Model { + return &customTicketModel{ + defaultTicketModel: newTicketModel(conn, c), + } +} + +// QueryTicketDetail returns the ticket details. +func (m *customTicketModel) QueryTicketDetail(ctx context.Context, id int64) (*Details, error) { + key := fmt.Sprintf("%s%v", cacheTicketDetailPrefix, id) + var data *Details + err := m.QueryCtx(ctx, &data, key, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Ticket{}).Where("id = ?", id).Preload("Follows").First(v).Error + }) + return data, err +} + +// InsertTicketFollow inserts a follow record. +func (m *customTicketModel) InsertTicketFollow(ctx context.Context, data *Follow) error { + key := fmt.Sprintf("%s%v", cacheTicketDetailPrefix, data.TicketId) + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Model(&Follow{}).Create(data).Error + }, key) +} + +// QueryTicketList returns the ticket list. +func (m *customTicketModel) QueryTicketList(ctx context.Context, page, size int, userId int64, status *uint8, search string) (int64, []*Ticket, error) { + var data []*Ticket + var total int64 + err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { + query := conn.Model(&Ticket{}) + if userId > 0 { + query = query.Where("user_id = ?", userId) + } + if status != nil { + query = query.Where("status = ?", status) + } else { + query = query.Where("status != ?", 4) + } + if search != "" { + query = query.Where("title like ? or description like ?", "%"+search+"%", "%"+search+"%") + } + return query.Count(&total).Order("id desc").Limit(size).Offset((page - 1) * size).Find(v).Error + }) + return total, data, err +} + +// UpdateTicketStatus updates the ticket status. +func (m *customTicketModel) UpdateTicketStatus(ctx context.Context, id, userId int64, status uint8) error { + key := fmt.Sprintf("%s%v", cacheTicketDetailPrefix, id) + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + conn = conn.Model(&Ticket{}) + if userId > 0 { + conn = conn.Where("user_id = ?", userId) + } + return conn.Where("id = ?", id).Update("status", status).Error + }, key) +} + +// QueryWaitReplyTotal returns the total number of tickets that are waiting for a reply. +func (m *customTicketModel) QueryWaitReplyTotal(ctx context.Context) (int64, error) { + var total int64 + err := m.QueryNoCacheCtx(ctx, &total, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Ticket{}).Where("status = ?", Pending).Count(&total).Error + }) + return total, err +} diff --git a/internal/model/ticket/ticket.go b/internal/model/ticket/ticket.go new file mode 100644 index 0000000..2c8f47d --- /dev/null +++ b/internal/model/ticket/ticket.go @@ -0,0 +1,37 @@ +package ticket + +import "time" + +const ( + Pending = 1 // Pending # Pending follow up + Waiting = 2 // Waiting # Waiting for user response + Processed = 3 // Processed + Closed = 4 // Closed +) + +type Ticket struct { + Id int64 `gorm:"primaryKey"` + Title string `gorm:"type:varchar(255);not null;default:'';comment:Title"` + Description string `gorm:"type:text;comment:Description"` + UserId int64 `gorm:"type:bigint;not null;default:0;comment:UserId"` + Status uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Status"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (Ticket) TableName() string { + return "ticket" +} + +type Follow struct { + Id int64 `gorm:"primaryKey"` + TicketId int64 `gorm:"type:bigint;not null;default:0;comment:TicketId"` + From string `gorm:"type:varchar(255);not null;default:'';comment:From"` + Type uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Type: 1 text, 2 image"` + Content string `gorm:"type:text;comment:Content"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` +} + +func (Follow) TableName() string { + return "ticket_follow" +} diff --git a/internal/model/traffic/default.go b/internal/model/traffic/default.go new file mode 100644 index 0000000..33e4f67 --- /dev/null +++ b/internal/model/traffic/default.go @@ -0,0 +1,69 @@ +package traffic + +import ( + "context" + "errors" + + "gorm.io/gorm" +) + +var _ Model = (*customTrafficModel)(nil) + +type ( + Model interface { + trafficModel + customTrafficLogicModel + } + trafficModel interface { + Insert(ctx context.Context, data *TrafficLog) error + FindOne(ctx context.Context, id int64) (*TrafficLog, error) + Update(ctx context.Context, data *TrafficLog) error + Delete(ctx context.Context, id int64) error + Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + } + + customTrafficModel struct { + *defaultTrafficModel + } + defaultTrafficModel struct { + Conn *gorm.DB + table string + } +) + +func newTrafficModel(db *gorm.DB) *defaultTrafficModel { + return &defaultTrafficModel{ + Conn: db, + table: "`traffic`", + } +} + +func (m *defaultTrafficModel) Insert(ctx context.Context, data *TrafficLog) error { + return m.Conn.WithContext(ctx).Create(&data).Error +} + +func (m *defaultTrafficModel) FindOne(ctx context.Context, id int64) (*TrafficLog, error) { + var data TrafficLog + err := m.Conn.WithContext(ctx).Model(&TrafficLog{}).Where("`id` = ?", id).First(&data).Error + return &data, err +} + +func (m *defaultTrafficModel) Update(ctx context.Context, data *TrafficLog) error { + return m.Conn.WithContext(ctx).Save(data).Error +} + +func (m *defaultTrafficModel) Delete(ctx context.Context, id int64) error { + _, err := m.FindOne(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + + return m.Conn.WithContext(ctx).Delete(&TrafficLog{}, id).Error +} + +func (m *defaultTrafficModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { + return m.Conn.WithContext(ctx).Transaction(fn) +} diff --git a/internal/model/traffic/model.go b/internal/model/traffic/model.go new file mode 100644 index 0000000..d9ef1bc --- /dev/null +++ b/internal/model/traffic/model.go @@ -0,0 +1,123 @@ +package traffic + +import ( + "context" + "time" + + "gorm.io/gorm" +) + +type customTrafficLogicModel interface { + QueryServerTrafficByDay(ctx context.Context, serverId int64, date time.Time) (*TotalTraffic, error) + QueryTrafficByDay(ctx context.Context, date time.Time) (*TotalTraffic, error) + QueryTrafficByMonthly(ctx context.Context, date time.Time) (*TotalTraffic, error) + TopServersTrafficByDay(ctx context.Context, date time.Time, limit int) ([]ServerTrafficRanking, error) + TopServersTrafficByMonthly(ctx context.Context, date time.Time, limit int) ([]ServerTrafficRanking, error) + TopUsersTrafficByDay(ctx context.Context, date time.Time, limit int) ([]UserTrafficRanking, error) + TopUsersTrafficByMonthly(ctx context.Context, date time.Time, limit int) ([]UserTrafficRanking, error) + QueryTrafficLogPageList(ctx context.Context, userId, subscribeId int64, page, size int) ([]*TrafficLog, int64, error) +} + +// NewModel returns a model for the database table. +func NewModel(conn *gorm.DB) Model { + return &customTrafficModel{ + defaultTrafficModel: newTrafficModel(conn), + } +} + +func (m *customTrafficModel) QueryServerTrafficByDay(ctx context.Context, serverId int64, date time.Time) (*TotalTraffic, error) { + var data TotalTraffic + start := date.Truncate(24 * time.Hour) + end := start.Add(24 * time.Hour).Add(-time.Nanosecond) + err := m.Conn.WithContext(ctx).Model(&TrafficLog{}). + Select("sum(download) as download, sum(upload) as upload"). + Where("server_id = ? AND timestamp BETWEEN ? AND ?", serverId, start, end). + Scan(&data).Error + return &data, err +} + +func (m *customTrafficModel) QueryTrafficByDay(ctx context.Context, date time.Time) (*TotalTraffic, error) { + var data TotalTraffic + start := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.Local) + end := start.Add(24 * time.Hour).Add(-time.Nanosecond) + err := m.Conn.WithContext(ctx).Model(&TrafficLog{}). + Select("sum(download) as download, sum(upload) as upload"). + Where("timestamp BETWEEN ? AND ?", start, end). + Scan(&data).Error + return &data, err +} + +func (m *customTrafficModel) QueryTrafficByMonthly(ctx context.Context, date time.Time) (*TotalTraffic, error) { + var data TotalTraffic + start := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, time.Local) + end := start.AddDate(0, 1, 0).Add(-time.Nanosecond) + err := m.Conn.WithContext(ctx).Model(&TrafficLog{}). + Select("sum(download) as download, sum(upload) as upload"). + Where("timestamp BETWEEN ? AND ?", start, end). + Scan(&data).Error + return &data, err +} + +func (m *customTrafficModel) TopServersTrafficByDay(ctx context.Context, date time.Time, limit int) ([]ServerTrafficRanking, error) { + var summaries []ServerTrafficRanking + start := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.Local) + end := start.Add(24 * time.Hour).Add(-time.Nanosecond) + err := m.Conn.Debug().WithContext(ctx).Model(&TrafficLog{}). + Select("server_id, SUM(download + upload) AS total, SUM(download) AS download, SUM(upload) AS upload"). + Where("timestamp BETWEEN ? AND ?", start, end). + Group("server_id"). + Order("total DESC"). + Limit(limit). + Scan(&summaries).Error + return summaries, err +} + +func (m *customTrafficModel) TopServersTrafficByMonthly(ctx context.Context, date time.Time, limit int) ([]ServerTrafficRanking, error) { + var summaries []ServerTrafficRanking + start := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, time.Local) + end := start.AddDate(0, 1, 0).Add(-time.Nanosecond) + err := m.Conn.WithContext(ctx).Model(&TrafficLog{}). + Select("server_id, SUM(download + upload) AS total, SUM(download) AS download, SUM(upload) AS upload"). + Where("timestamp BETWEEN ? AND ?", start, end). + Group("server_id"). + Order("total DESC"). + Limit(limit). + Scan(&summaries).Error + return summaries, err +} + +func (m *customTrafficModel) TopUsersTrafficByDay(ctx context.Context, date time.Time, limit int) ([]UserTrafficRanking, error) { + var summaries []UserTrafficRanking + start := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.Local) + end := start.Add(24 * time.Hour).Add(-time.Nanosecond) + err := m.Conn.WithContext(ctx).Model(&TrafficLog{}). + Select("user_id, subscribe_id, SUM(download + upload) AS total, SUM(download) AS download, SUM(upload) AS upload"). + Where("timestamp BETWEEN ? AND ?", start, end). + Group("user_id, subscribe_id"). // 修改这里,添加 subscribe_id 到 GROUP BY 子句 + Order("total DESC"). + Limit(limit). + Scan(&summaries).Error + return summaries, err +} + +func (m *customTrafficModel) TopUsersTrafficByMonthly(ctx context.Context, date time.Time, limit int) ([]UserTrafficRanking, error) { + var summaries []UserTrafficRanking + start := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, time.Local) + end := start.AddDate(0, 1, 0).Add(-time.Nanosecond) + err := m.Conn.WithContext(ctx).Model(&TrafficLog{}). + Select("user_id, subscribe_id, SUM(download + upload) AS total, SUM(download) AS download, SUM(upload) AS upload"). // 添加 subscribe_id 到 SELECT 列表 + Where("timestamp BETWEEN ? AND ?", start, end). + Group("user_id, subscribe_id"). // 修改这里,添加 subscribe_id 到 GROUP BY 子句 + Order("total DESC"). + Limit(limit). + Scan(&summaries).Error + return summaries, err +} + +// QueryTrafficLogPageList returns a list of records that meet the conditions. +func (m *customTrafficModel) QueryTrafficLogPageList(ctx context.Context, userId, subscribeId int64, page, size int) ([]*TrafficLog, int64, error) { + var list []*TrafficLog + var total int64 + err := m.Conn.WithContext(ctx).Model(&TrafficLog{}).Where("user_id = ? and subscribe_id= ?", userId, subscribeId).Count(&total).Limit(size).Offset((page - 1) * size).Find(&list).Error + return list, total, err +} diff --git a/internal/model/traffic/traffic.go b/internal/model/traffic/traffic.go new file mode 100644 index 0000000..e61d3ad --- /dev/null +++ b/internal/model/traffic/traffic.go @@ -0,0 +1,38 @@ +package traffic + +import "time" + +//goland:noinspection GoNameStartsWithPackageName +type TrafficLog struct { + Id int64 `gorm:"primaryKey"` + ServerId int64 `gorm:"index:idx_server_id;not null;comment:Server ID"` + UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` + SubscribeId int64 `gorm:"index:idx_subscribe_id;not null;comment:Subscription ID"` + Download int64 `gorm:"default:0;comment:Download Traffic"` + Upload int64 `gorm:"default:0;comment:Upload Traffic"` + Timestamp time.Time `gorm:"default:CURRENT_TIMESTAMP(3);not null;comment:Traffic Log Time"` +} + +type TotalTraffic struct { + Download int64 + Upload int64 +} + +type ServerTrafficRanking struct { + ServerId int64 + Download int64 + Upload int64 + Total int64 +} + +type UserTrafficRanking struct { + UserId int64 + SubscribeId int64 + Download int64 + Upload int64 + Total int64 +} + +func (TrafficLog) TableName() string { + return "traffic_log" +} diff --git a/internal/model/user/authMethod.go b/internal/model/user/authMethod.go new file mode 100644 index 0000000..07faabd --- /dev/null +++ b/internal/model/user/authMethod.go @@ -0,0 +1,66 @@ +package user + +import ( + "context" + + "gorm.io/gorm" +) + +func (m *defaultUserModel) FindUserAuthMethods(ctx context.Context, userId int64) ([]*AuthMethods, error) { + var data []*AuthMethods + err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&AuthMethods{}).Where("user_id = ?", userId).Find(&data).Error + }) + return data, err +} + +func (m *defaultUserModel) FindUserAuthMethodByOpenID(ctx context.Context, method, openID string) (*AuthMethods, error) { + var data AuthMethods + err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&AuthMethods{}).Where("auth_type = ? AND auth_identifier = ?", method, openID).First(&data).Error + }) + return &data, err +} + +func (m *defaultUserModel) FindUserAuthMethodByPlatform(ctx context.Context, userId int64, platform string) (*AuthMethods, error) { + var data AuthMethods + err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&AuthMethods{}).Where("user_id = ? AND auth_type = ?", userId, platform).First(&data).Error + }) + return &data, err +} + +func (m *defaultUserModel) InsertUserAuthMethods(ctx context.Context, data *AuthMethods, tx ...*gorm.DB) error { + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Model(&AuthMethods{}).Create(data).Error + }) +} + +func (m *defaultUserModel) UpdateUserAuthMethods(ctx context.Context, data *AuthMethods, tx ...*gorm.DB) error { + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Model(&AuthMethods{}).Where("user_id = ? AND auth_type = ?", data.UserId, data.AuthType).Save(data).Error + }) +} + +func (m *defaultUserModel) DeleteUserAuthMethods(ctx context.Context, userId int64, platform string, tx ...*gorm.DB) error { + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Model(&AuthMethods{}).Where("user_id = ? AND auth_type = ?", userId, platform).Delete(&AuthMethods{}).Error + }) +} + +func (m *defaultUserModel) FindUserAuthMethodByUserId(ctx context.Context, method string, userId int64) (*AuthMethods, error) { + var data AuthMethods + err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&AuthMethods{}).Where("auth_type = ? AND user_id = ?", method, userId).First(&data).Error + }) + return &data, err +} diff --git a/internal/model/user/default.go b/internal/model/user/default.go new file mode 100644 index 0000000..07ebc16 --- /dev/null +++ b/internal/model/user/default.go @@ -0,0 +1,181 @@ +package user + +import ( + "context" + "errors" + "fmt" + + "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var ( + cacheUserIdPrefix = "cache:user:id:" + cacheUserEmailPrefix = "cache:user:email:" +) +var _ Model = (*customUserModel)(nil) + +type ( + Model interface { + userModel + customUserLogicModel + } + userModel interface { + Insert(ctx context.Context, data *User, tx ...*gorm.DB) error + FindOne(ctx context.Context, id int64) (*User, error) + Update(ctx context.Context, data *User, tx ...*gorm.DB) error + Delete(ctx context.Context, id int64, tx ...*gorm.DB) error + Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + } + + customUserModel struct { + *defaultUserModel + } + defaultUserModel struct { + cache.CachedConn + table string + } +) + +func newUserModel(db *gorm.DB, c *redis.Client) *defaultUserModel { + return &defaultUserModel{ + CachedConn: cache.NewConn(db, c), + table: "`user`", + } +} + +func (m *defaultUserModel) batchGetCacheKeys(users ...*User) []string { + var keys []string + for _, user := range users { + keys = append(keys, m.getCacheKeys(user)...) + } + return keys + +} +func (m *defaultUserModel) getCacheKeys(data *User) []string { + if data == nil { + return []string{} + } + userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, data.Id) + cacheKeys := []string{ + userIdKey, + } + // email key + if len(data.AuthMethods) > 0 { + for _, auth := range data.AuthMethods { + if auth.AuthType == "email" { + cacheKeys = append(cacheKeys, fmt.Sprintf("%s%v", cacheUserEmailPrefix, auth.AuthIdentifier)) + break + } + } + } + return cacheKeys +} + +func (m *defaultUserModel) FindOneByEmail(ctx context.Context, email string) (*User, error) { + var user User + key := fmt.Sprintf("%s%v", cacheUserEmailPrefix, email) + err := m.QueryCtx(ctx, &user, key, func(conn *gorm.DB, v interface{}) error { + var data AuthMethods + if err := conn.Model(&AuthMethods{}).Where("`auth_type` = 'email' AND `auth_identifier` = ?", email).First(&data).Error; err != nil { + return err + } + return conn.Model(&User{}).Where("`id` = ?", data.UserId).Preload("UserDevices").Preload("AuthMethods").First(v).Error + }) + return &user, err +} + +func (m *defaultUserModel) Insert(ctx context.Context, data *User, tx ...*gorm.DB) error { + err := m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Create(&data).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultUserModel) FindOne(ctx context.Context, id int64) (*User, error) { + userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id) + var resp User + err := m.QueryCtx(ctx, &resp, userIdKey, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&User{}).Where("`id` = ?", id).Preload("UserDevices").Preload("AuthMethods").First(&resp).Error + }) + return &resp, err +} + +func (m *defaultUserModel) Update(ctx context.Context, data *User, tx ...*gorm.DB) error { + old, err := m.FindOne(ctx, data.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Save(data).Error + }, m.getCacheKeys(old)...) + return err +} + +func (m *defaultUserModel) Delete(ctx context.Context, id int64, tx ...*gorm.DB) error { + data, err := m.FindOne(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Transaction(func(db *gorm.DB) error { + if err := db.Model(&User{}).Where("`id` = ?", id).Delete(&User{}).Error; err != nil { + return err + } + if err := db.Model(&AuthMethods{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { + return err + } + if err := db.Model(&Subscribe{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { + return err + } + if err := db.Model(&BalanceLog{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { + return err + } + if err := db.Model(&GiftAmountLog{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { + return err + } + if err := db.Model(&LoginLog{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { + return err + } + if err := db.Model(&SubscribeLog{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { + return err + } + if err := db.Model(&Device{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { + return err + } + + subs, err := m.QueryUserSubscribe(ctx, id) + if err != nil { + return err + } + for _, sub := range subs { + if err := m.DeleteSubscribeById(ctx, sub.Id, db); err != nil { + return err + } + } + + if err := db.Model(&CommissionLog{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { + return err + } + return nil + }) + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultUserModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { + return m.TransactCtx(ctx, fn) +} diff --git a/internal/model/user/device.go b/internal/model/user/device.go new file mode 100644 index 0000000..7258819 --- /dev/null +++ b/internal/model/user/device.go @@ -0,0 +1,80 @@ +package user + +import ( + "context" + "errors" + "fmt" + + "gorm.io/gorm" +) + +func (m *customUserModel) FindOneDevice(ctx context.Context, id int64) (*Device, error) { + deviceIdKey := fmt.Sprintf("%s%v", cacheUserDeviceIdPrefix, id) + var resp Device + err := m.QueryCtx(ctx, &resp, deviceIdKey, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Device{}).Where("`id` = ?", id).First(&resp).Error + }) + switch { + case err == nil: + return &resp, nil + default: + return nil, err + } +} + +func (m *customUserModel) FindOneDeviceByIdentifier(ctx context.Context, id string) (*Device, error) { + deviceIdKey := fmt.Sprintf("%s%v", cacheUserDeviceNumberPrefix, id) + var resp Device + err := m.QueryCtx(ctx, &resp, deviceIdKey, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Device{}).Where("`identifier` = ?", id).First(&resp).Error + }) + switch { + case err == nil: + return &resp, nil + default: + return nil, err + } +} + +// QueryDevicePageList returns a list of records that meet the conditions. +func (m *customUserModel) QueryDevicePageList(ctx context.Context, userId, subscribeId int64, page, size int) ([]*Device, int64, error) { + var list []*Device + var total int64 + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Device{}).Where("`user_id` = ? and `subscribe_id` = ?", userId, subscribeId).Count(&total).Limit(size).Offset((page - 1) * size).Find(&list).Error + }) + return list, total, err +} + +func (m *customUserModel) UpdateDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error { + old, err := m.FindOneDevice(ctx, data.Id) + if err != nil { + return err + } + deviceIdKey := fmt.Sprintf("%s%v", cacheUserDeviceIdPrefix, old.Id) + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Save(data).Error + }, deviceIdKey) + return err +} + +func (m *customUserModel) DeleteDevice(ctx context.Context, id int64, tx ...*gorm.DB) error { + data, err := m.FindOneDevice(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + deviceIdKey := fmt.Sprintf("%s%v", cacheUserDeviceIdPrefix, data.Id) + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Delete(&Device{}, id).Error + }, deviceIdKey) + return err +} diff --git a/internal/model/user/log.go b/internal/model/user/log.go new file mode 100644 index 0000000..d3e1105 --- /dev/null +++ b/internal/model/user/log.go @@ -0,0 +1,81 @@ +package user + +import ( + "context" + + "github.com/pkg/errors" + "gorm.io/gorm" +) + +func (m *customUserModel) InsertSubscribeLog(ctx context.Context, log *SubscribeLog) error { + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { + return conn.Create(log).Error + }) +} + +func (m *customUserModel) FilterSubscribeLogList(ctx context.Context, page, size int, filter *SubscribeLogFilterParams) ([]*SubscribeLog, int64, error) { + var list []*SubscribeLog + var total int64 + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + query := conn.Model(&SubscribeLog{}) + if filter != nil { + if filter.UserId != 0 { + query = query.Where("user_id = ?", filter.UserId) + } + if filter.UserSubscribeId != 0 { + query = query.Where("user_subscribe_id = ?", filter.UserSubscribeId) + } + if filter.IP != "" { + query = query.Where("ip LIKE ?", "%"+filter.IP+"%") + } + if filter.Token != "" { + query = query.Where("token LIKE ?", "%"+filter.Token+"%") + } + if filter.UserAgent != "" { + query = query.Where("user_agent LIKE ?", "%"+filter.UserAgent+"%") + } + } + return query.Count(&total).Limit(size).Offset((page - 1) * size).Find(v).Error + }) + + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, 0, err + } + + return list, total, nil +} + +func (m *customUserModel) InsertLoginLog(ctx context.Context, log *LoginLog) error { + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { + return conn.Create(log).Error + }) +} + +func (m *customUserModel) FilterLoginLogList(ctx context.Context, page, size int, filter *LoginLogFilterParams) ([]*LoginLog, int64, error) { + var list []*LoginLog + var total int64 + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + query := conn.Model(&LoginLog{}) + if filter != nil { + if filter.UserId != 0 { + query = query.Where("user_id = ?", filter.UserId) + } + if filter.IP != "" { + query = query.Where("ip LIKE ?", "%"+filter.IP+"%") + } + if filter.UserAgent != "" { + query = query.Where("user_agent LIKE ?", "%"+filter.UserAgent+"%") + } + if filter.Success != nil { + query = query.Where("success = ?", *filter.Success) + } + } + return query.Count(&total).Limit(size).Offset((page - 1) * size).Find(v).Error + }) + + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, 0, err + } + + return list, total, nil +} diff --git a/internal/model/user/model.go b/internal/model/user/model.go new file mode 100644 index 0000000..3175331 --- /dev/null +++ b/internal/model/user/model.go @@ -0,0 +1,400 @@ +package user + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/model/server" + "github.com/perfect-panel/ppanel-server/internal/model/subscribe" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +const ( + cacheUserSubscribeTokenPrefix = "cache:user:subscribe:token:" + cacheUserSubscribeUserPrefix = "cache:user:subscribe:user:" + cacheUserSubscribeIdPrefix = "cache:user:subscribe:id:" + cacheUserDeviceNumberPrefix = "cache:user:device:number:" + cacheUserDeviceIdPrefix = "cache:user:device:id:" +) + +type SubscribeDetails struct { + Id int64 `gorm:"primarykey"` + UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` + User *User `gorm:"foreignKey:UserId;references:Id"` + OrderId int64 `gorm:"index:idx_order_id;not null;comment:Order ID"` + SubscribeId int64 `gorm:"index:idx_subscribe_id;not null;comment:Subscription ID"` + Subscribe *subscribe.Subscribe `gorm:"foreignKey:SubscribeId;references:Id"` + StartTime time.Time `gorm:"default:CURRENT_TIMESTAMP(3);not null;comment:Subscription Start Time"` + ExpireTime time.Time `gorm:"default:NULL;comment:Subscription Expire Time"` + Traffic int64 `gorm:"default:0;comment:Traffic"` + Download int64 `gorm:"default:0;comment:Download Traffic"` + Upload int64 `gorm:"default:0;comment:Upload Traffic"` + Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"` + UUID string `gorm:"type:varchar(255);unique;index:idx_uuid;default:'';comment:UUID"` + Status uint8 `gorm:"type:tinyint(1);default:0;comment:Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired; 4: Cancelled"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +type SubscribeLogFilterParams struct { + IP string + UserAgent string + UserId int64 + Token string + UserSubscribeId int64 +} + +type LoginLogFilterParams struct { + IP string + UserId int64 + UserAgent string + Success *bool +} + +type UserFilterParams struct { + Search string + UserId *int64 + SubscribeId *int64 + UserSubscribeId *int64 +} + +type customUserLogicModel interface { + QueryPageList(ctx context.Context, page, size int, filter *UserFilterParams) ([]*User, int64, error) + FindOneByReferCode(ctx context.Context, referCode string) (*User, error) + BatchDeleteUser(ctx context.Context, ids []int64, tx ...*gorm.DB) error + InsertSubscribe(ctx context.Context, data *Subscribe, tx ...*gorm.DB) error + FindOneSubscribeByToken(ctx context.Context, token string) (*Subscribe, error) + FindOneSubscribeByOrderId(ctx context.Context, orderId int64) (*Subscribe, error) + FindOneSubscribe(ctx context.Context, id int64) (*Subscribe, error) + UpdateSubscribe(ctx context.Context, data *Subscribe, tx ...*gorm.DB) error + DeleteSubscribe(ctx context.Context, token string, tx ...*gorm.DB) error + DeleteSubscribeById(ctx context.Context, id int64, tx ...*gorm.DB) error + QueryUserSubscribe(ctx context.Context, userId int64, status ...int64) ([]*SubscribeDetails, error) + FindOneSubscribeDetailsById(ctx context.Context, id int64) (*SubscribeDetails, error) + FindOneUserSubscribe(ctx context.Context, id int64) (*SubscribeDetails, error) + InsertBalanceLog(ctx context.Context, data *BalanceLog, tx ...*gorm.DB) error + FindUsersSubscribeBySubscribeId(ctx context.Context, subscribeId int64) ([]*Subscribe, error) + UpdateUserSubscribeWithTraffic(ctx context.Context, id, download, upload int64, tx ...*gorm.DB) error + QueryResisterUserTotalByDate(ctx context.Context, date time.Time) (int64, error) + QueryResisterUserTotalByMonthly(ctx context.Context, date time.Time) (int64, error) + QueryResisterUserTotal(ctx context.Context) (int64, error) + QueryAdminUsers(ctx context.Context) ([]*User, error) + UpdateUserCache(ctx context.Context, data *User) error + UpdateUserSubscribeCache(ctx context.Context, data *Subscribe) error + InsertCommissionLog(ctx context.Context, data *CommissionLog, tx ...*gorm.DB) error + QueryActiveSubscriptions(ctx context.Context, subscribeId ...int64) (map[int64]int64, error) + FindUserAuthMethods(ctx context.Context, userId int64) ([]*AuthMethods, error) + InsertUserAuthMethods(ctx context.Context, data *AuthMethods, tx ...*gorm.DB) error + UpdateUserAuthMethods(ctx context.Context, data *AuthMethods, tx ...*gorm.DB) error + DeleteUserAuthMethods(ctx context.Context, userId int64, platform string, tx ...*gorm.DB) error + FindUserAuthMethodByOpenID(ctx context.Context, method, openID string) (*AuthMethods, error) + FindUserAuthMethodByUserId(ctx context.Context, method string, userId int64) (*AuthMethods, error) + FindUserAuthMethodByPlatform(ctx context.Context, userId int64, platform string) (*AuthMethods, error) + FindOneByEmail(ctx context.Context, email string) (*User, error) + FindOneDevice(ctx context.Context, id int64) (*Device, error) + QueryDevicePageList(ctx context.Context, userid, subscribeId int64, page, size int) ([]*Device, int64, error) + UpdateDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error + FindOneDeviceByIdentifier(ctx context.Context, id string) (*Device, error) + DeleteDevice(ctx context.Context, id int64, tx ...*gorm.DB) error + + InsertSubscribeLog(ctx context.Context, log *SubscribeLog) error + FilterSubscribeLogList(ctx context.Context, page, size int, filter *SubscribeLogFilterParams) ([]*SubscribeLog, int64, error) + InsertLoginLog(ctx context.Context, log *LoginLog) error + FilterLoginLogList(ctx context.Context, page, size int, filter *LoginLogFilterParams) ([]*LoginLog, int64, error) + + ClearSubscribeCache(ctx context.Context, data ...*Subscribe) error + + InsertResetSubscribeLog(ctx context.Context, log *ResetSubscribeLog, tx ...*gorm.DB) error + UpdateResetSubscribeLog(ctx context.Context, log *ResetSubscribeLog, tx ...*gorm.DB) error + FindResetSubscribeLog(ctx context.Context, id int64) (*ResetSubscribeLog, error) + DeleteResetSubscribeLog(ctx context.Context, id int64, tx ...*gorm.DB) error + FilterResetSubscribeLogList(ctx context.Context, filter *FilterResetSubscribeLogParams) ([]*ResetSubscribeLog, int64, error) +} + +// NewModel returns a model for the database table. +func NewModel(conn *gorm.DB, c *redis.Client) Model { + return &customUserModel{ + defaultUserModel: newUserModel(conn, c), + } +} + +func (m *defaultUserModel) getSubscribeCacheKey(data *Subscribe) []string { + if data == nil { + return []string{} + } + var keys []string + if data.Token != "" { + keys = append(keys, fmt.Sprintf("%s%s", cacheUserSubscribeTokenPrefix, data.Token)) + } + if data.UserId != 0 { + keys = append(keys, fmt.Sprintf("%s%d", cacheUserSubscribeUserPrefix, data.UserId)) + } + if data.Id != 0 { + keys = append(keys, fmt.Sprintf("%s%d", cacheUserSubscribeIdPrefix, data.Id)) + } + + if data.SubscribeId != 0 { + var sub *subscribe.Subscribe + err := m.QueryNoCacheCtx(context.Background(), &sub, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&subscribe.Subscribe{}).Where("id = ?", data.SubscribeId).First(&sub).Error + }) + if err != nil { + logger.Error("getUserSubscribeCacheKey", logger.Field("error", err.Error()), logger.Field("subscribeId", data.SubscribeId)) + return keys + } + if sub.Server != "" { + ids := tool.StringToInt64Slice(sub.Server) + for _, id := range ids { + keys = append(keys, fmt.Sprintf("%s%d", config.ServerUserListCacheKey, id)) + } + } + if sub.ServerGroup != "" { + ids := tool.StringToInt64Slice(sub.ServerGroup) + var servers []*server.Server + err = m.QueryNoCacheCtx(context.Background(), &servers, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&server.Server{}).Where("group_id in ?", ids).Find(v).Error + }) + if err != nil { + logger.Error("getUserSubscribeCacheKey", logger.Field("error", err.Error()), logger.Field("subscribeId", data.SubscribeId)) + return keys + } + for _, s := range servers { + keys = append(keys, fmt.Sprintf("%s%d", config.ServerUserListCacheKey, s.Id)) + } + } + } + + return keys + +} + +// QueryPageList returns a list of records that meet the conditions. +func (m *customUserModel) QueryPageList(ctx context.Context, page, size int, filter *UserFilterParams) ([]*User, int64, error) { + var list []*User + var total int64 + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + if filter != nil { + if filter.UserId != nil { + conn = conn.Where("user.id =?", *filter.UserId) + } + if filter.Search != "" { + conn = conn.Joins("LEFT JOIN user_auth_methods ON user.id = user_auth_methods.user_id"). + Where("user_auth_methods.auth_identifier LIKE ?", "%"+filter.Search+"%").Or("user.refer_code like ?", "%"+filter.Search+"%") + } + if filter.UserSubscribeId != nil { + conn = conn.Joins("LEFT JOIN user_subscribe ON user.id = user_subscribe.user_id"). + Where("user_subscribe.id =? and `status` IN (0,1)", *filter.UserSubscribeId) + } + if filter.SubscribeId != nil { + conn = conn.Joins("LEFT JOIN user_subscribe ON user.id = user_subscribe.user_id"). + Where("user_subscribe.subscribe_id =? and `status` IN (0,1)", *filter.SubscribeId) + } + } + return conn.Model(&User{}).Group("user.id").Count(&total).Limit(size).Offset((page - 1) * size).Preload("UserDevices").Preload("AuthMethods").Find(&list).Error + }) + return list, total, err +} + +// BatchDeleteUser deletes multiple records by primary key. +func (m *customUserModel) BatchDeleteUser(ctx context.Context, ids []int64, tx ...*gorm.DB) error { + var users []*User + err := m.QueryNoCacheCtx(ctx, &users, func(conn *gorm.DB, v interface{}) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Where("id in ?", ids).Find(&users).Error + }) + if err != nil { + return err + } + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Where("id in ?", ids).Delete(&User{}).Error + }, m.batchGetCacheKeys(users...)...) +} + +// InsertBalanceLog insert BalanceLog into the database. +func (m *customUserModel) InsertBalanceLog(ctx context.Context, data *BalanceLog, tx ...*gorm.DB) error { + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Create(data).Error + }) +} + +// FindUserBalanceLogList returns a list of records that meet the conditions. +func (m *customUserModel) FindUserBalanceLogList(ctx context.Context, userId int64, page, size int) ([]*BalanceLog, int64, error) { + var list []*BalanceLog + var total int64 + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + + return conn.Model(&BalanceLog{}).Where("`user_id` = ?", userId).Count(&total).Limit(size).Offset((page - 1) * size).Find(&list).Error + }) + return list, total, err +} + +func (m *customUserModel) UpdateUserSubscribeWithTraffic(ctx context.Context, id, download, upload int64, tx ...*gorm.DB) error { + sub, err := m.FindOneSubscribe(ctx, id) + if err != nil { + return err + } + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Model(&Subscribe{}).Where("id = ?", id).Updates(map[string]interface{}{ + "download": gorm.Expr("download + ?", download), + "upload": gorm.Expr("upload + ?", upload), + }).Error + }, m.getSubscribeCacheKey(sub)...) +} + +func (m *customUserModel) QueryResisterUserTotalByDate(ctx context.Context, date time.Time) (int64, error) { + var total int64 + start := date.Truncate(24 * time.Hour) + end := start.Add(24 * time.Hour).Add(-time.Second) + err := m.QueryNoCacheCtx(ctx, &total, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&User{}).Where("created_at > ? and created_at < ?", start, end).Count(&total).Error + }) + return total, err +} + +func (m *customUserModel) QueryResisterUserTotalByMonthly(ctx context.Context, date time.Time) (int64, error) { + var total int64 + start := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, time.Local) + end := start.AddDate(0, 1, 0).Add(-time.Nanosecond) + err := m.QueryNoCacheCtx(ctx, &total, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&User{}).Where("created_at > ? and created_at < ?", start, end).Count(&total).Error + }) + return total, err +} + +func (m *customUserModel) QueryResisterUserTotal(ctx context.Context) (int64, error) { + var total int64 + err := m.QueryNoCacheCtx(ctx, &total, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&User{}).Count(&total).Error + }) + return total, err +} + +func (m *customUserModel) QueryAdminUsers(ctx context.Context) ([]*User, error) { + var data []*User + err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&User{}).Preload("AuthMethods").Where("is_admin = ?", true).Find(&data).Error + }) + return data, err +} + +func (m *customUserModel) UpdateUserCache(ctx context.Context, data *User) error { + return m.CachedConn.DelCacheCtx(ctx, m.getCacheKeys(data)...) +} + +func (m *customUserModel) InsertCommissionLog(ctx context.Context, data *CommissionLog, tx ...*gorm.DB) error { + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Model(&CommissionLog{}).Create(data).Error + }) +} + +func (m *customUserModel) FindOneByReferCode(ctx context.Context, referCode string) (*User, error) { + var data User + err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&User{}).Where("refer_code = ?", referCode).First(&data).Error + }) + return &data, err +} + +func (m *customUserModel) FindOneSubscribeDetailsById(ctx context.Context, id int64) (*SubscribeDetails, error) { + var data SubscribeDetails + err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Subscribe{}).Preload("Subscribe").Preload("User").Where("id = ?", id).First(&data).Error + }) + return &data, err +} + +func (m *customUserModel) InsertResetSubscribeLog(ctx context.Context, log *ResetSubscribeLog, tx ...*gorm.DB) error { + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Model(&ResetSubscribeLog{}).Create(log).Error + }) +} + +func (m *customUserModel) UpdateResetSubscribeLog(ctx context.Context, log *ResetSubscribeLog, tx ...*gorm.DB) error { + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Model(&ResetSubscribeLog{}).Where("id = ?", log.Id).Updates(log).Error + }) +} + +func (m *customUserModel) FindResetSubscribeLog(ctx context.Context, id int64) (*ResetSubscribeLog, error) { + var data ResetSubscribeLog + err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&ResetSubscribeLog{}).Where("id = ?", id).First(&data).Error + }) + return &data, err +} + +func (m *customUserModel) DeleteResetSubscribeLog(ctx context.Context, id int64, tx ...*gorm.DB) error { + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Model(&ResetSubscribeLog{}).Where("id = ?", id).Delete(&ResetSubscribeLog{}).Error + }) +} + +func (m *customUserModel) FilterResetSubscribeLogList(ctx context.Context, filter *FilterResetSubscribeLogParams) ([]*ResetSubscribeLog, int64, error) { + if filter == nil { + return nil, 0, errors.New("filter params is nil") + } + + var list []*ResetSubscribeLog + var total int64 + + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + query := conn.Model(&ResetSubscribeLog{}) + + // 应用筛选条件 + if filter.UserId != 0 { + query = query.Where("user_id = ?", filter.UserId) + } + if filter.UserSubscribeId != 0 { + query = query.Where("user_subscribe_id = ?", filter.UserSubscribeId) + } + if filter.Type != 0 { + query = query.Where("type = ?", filter.Type) + } + if filter.OrderNo != "" { + query = query.Where("order_no = ?", filter.OrderNo) + } + + // 计算总数 + if err := query.Count(&total).Error; err != nil { + return err + } + + // 应用分页 + if filter.Page > 0 && filter.Size > 0 { + query = query.Offset((filter.Page - 1) * filter.Size) + } + if filter.Size > 0 { + query = query.Limit(filter.Size) + } + + return query.Find(&list).Error + }) + + return list, total, err +} diff --git a/internal/model/user/subscribe.go b/internal/model/user/subscribe.go new file mode 100644 index 0000000..0c8d5ba --- /dev/null +++ b/internal/model/user/subscribe.go @@ -0,0 +1,160 @@ +package user + +import ( + "context" + "fmt" + "time" + + "gorm.io/gorm" +) + +func (m *defaultUserModel) UpdateUserSubscribeCache(ctx context.Context, data *Subscribe) error { + return m.CachedConn.DelCacheCtx(ctx, m.getSubscribeCacheKey(data)...) +} + +// QueryActiveSubscriptions returns the number of active subscriptions. +func (m *defaultUserModel) QueryActiveSubscriptions(ctx context.Context, subscribeId ...int64) (map[int64]int64, error) { + type SubscriptionCount struct { + SubscribeId int64 + Total int64 + } + var result []SubscriptionCount + err := m.QueryNoCacheCtx(ctx, &result, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Subscribe{}). + Where("subscribe_id IN ? AND `status` IN ?", subscribeId, []int64{1, 0, 3}). + Select("subscribe_id, COUNT(id) as total"). + Group("subscribe_id"). + Scan(&result). + Error + }) + + if err != nil { + return nil, err + } + + resultMap := make(map[int64]int64) + for _, item := range result { + resultMap[item.SubscribeId] = item.Total + } + + return resultMap, nil +} + +func (m *defaultUserModel) FindOneSubscribeByOrderId(ctx context.Context, orderId int64) (*Subscribe, error) { + var data Subscribe + err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Subscribe{}).Where("order_id = ?", orderId).First(&data).Error + }) + return &data, err +} + +func (m *defaultUserModel) FindOneSubscribe(ctx context.Context, id int64) (*Subscribe, error) { + var data Subscribe + key := fmt.Sprintf("%s%d", cacheUserSubscribeIdPrefix, id) + err := m.QueryCtx(ctx, &data, key, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Subscribe{}).Where("id = ?", id).First(&data).Error + }) + return &data, err + +} + +func (m *defaultUserModel) FindUsersSubscribeBySubscribeId(ctx context.Context, subscribeId int64) ([]*Subscribe, error) { + var data []*Subscribe + err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Subscribe{}).Where("subscribe_id = ? AND `status` IN ?", subscribeId, []int64{1, 0}).Find(&data).Error + }) + return data, err +} + +// QueryUserSubscribe returns a list of records that meet the conditions. +func (m *defaultUserModel) QueryUserSubscribe(ctx context.Context, userId int64, status ...int64) ([]*SubscribeDetails, error) { + var list []*SubscribeDetails + key := fmt.Sprintf("%s%d", cacheUserSubscribeUserPrefix, userId) + err := m.QueryCtx(ctx, &list, key, func(conn *gorm.DB, v interface{}) error { + // 获取当前时间 + now := time.Now() + // 获取当前时间向前推 7 天 + sevenDaysAgo := time.Now().Add(-7 * 24 * time.Hour) + // 基础条件查询 + conn = conn.Model(&Subscribe{}).Where("`user_id` = ? and `status` IN ?", userId, status) + return conn.Where("`expire_time` > ? OR `finished_at` >= ?", now, sevenDaysAgo). + Preload("Subscribe"). + Find(&list).Error + }) + return list, err +} + +// FindOneUserSubscribe finds a subscribeDetails by id. +func (m *defaultUserModel) FindOneUserSubscribe(ctx context.Context, id int64) (subscribeDetails *SubscribeDetails, err error) { + //TODO cache + //key := fmt.Sprintf("%s%d", cacheUserSubscribeUserPrefix, userId) + err = m.QueryNoCacheCtx(ctx, subscribeDetails, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Subscribe{}).Preload("Subscribe").Where("id = ?", id).First(&subscribeDetails).Error + }) + return +} + +// FindOneSubscribeByToken finds a record by token. +func (m *defaultUserModel) FindOneSubscribeByToken(ctx context.Context, token string) (*Subscribe, error) { + var data Subscribe + key := fmt.Sprintf("%s%s", cacheUserSubscribeTokenPrefix, token) + err := m.QueryCtx(ctx, &data, key, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Subscribe{}).Where("token = ?", token).First(&data).Error + }) + return &data, err +} + +// UpdateSubscribe updates a record. +func (m *defaultUserModel) UpdateSubscribe(ctx context.Context, data *Subscribe, tx ...*gorm.DB) error { + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Model(&Subscribe{}).Where("token = ?", data.Token).Save(data).Error + }, m.getSubscribeCacheKey(data)...) +} + +// DeleteSubscribe deletes a record. +func (m *defaultUserModel) DeleteSubscribe(ctx context.Context, token string, tx ...*gorm.DB) error { + data, err := m.FindOneSubscribeByToken(ctx, token) + if err != nil { + return err + } + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Where("token = ?", token).Delete(&Subscribe{}).Error + }, m.getSubscribeCacheKey(data)...) +} + +// InsertSubscribe insert Subscribe into the database. +func (m *defaultUserModel) InsertSubscribe(ctx context.Context, data *Subscribe, tx ...*gorm.DB) error { + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Create(data).Error + }, m.getSubscribeCacheKey(data)...) +} + +func (m *defaultUserModel) DeleteSubscribeById(ctx context.Context, id int64, tx ...*gorm.DB) error { + data, err := m.FindOneSubscribe(ctx, id) + if err != nil { + return err + } + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Where("id = ?", id).Delete(&Subscribe{}).Error + }, m.getSubscribeCacheKey(data)...) +} + +func (m *defaultUserModel) ClearSubscribeCache(ctx context.Context, data ...*Subscribe) error { + var keys []string + for _, item := range data { + keys = append(keys, m.getSubscribeCacheKey(item)...) + } + return m.CachedConn.DelCacheCtx(ctx, keys...) +} diff --git a/internal/model/user/user.go b/internal/model/user/user.go new file mode 100644 index 0000000..5fff3ee --- /dev/null +++ b/internal/model/user/user.go @@ -0,0 +1,230 @@ +package user + +import ( + "time" + + "gorm.io/gorm" + "gorm.io/plugin/soft_delete" +) + +type User struct { + Id int64 `gorm:"primaryKey"` + Password string `gorm:"type:varchar(100);not null;comment:User Password"` + Avatar string `gorm:"type:MEDIUMTEXT;comment:User Avatar"` + Balance int64 `gorm:"default:0;comment:User Balance"` // User Balance Amount + ReferCode string `gorm:"type:varchar(20);default:'';comment:Referral Code"` + RefererId int64 `gorm:"index:idx_referer;comment:Referrer ID"` + Commission int64 `gorm:"default:0;comment:Commission"` // Commission Amount + GiftAmount int64 `gorm:"default:0;comment:User Gift Amount"` + Enable *bool `gorm:"default:true;not null;comment:Is Account Enabled"` + IsAdmin *bool `gorm:"default:false;not null;comment:Is Admin"` + EnableBalanceNotify *bool `gorm:"default:false;not null;comment:Enable Balance Change Notifications"` + EnableLoginNotify *bool `gorm:"default:false;not null;comment:Enable Login Notifications"` + EnableSubscribeNotify *bool `gorm:"default:false;not null;comment:Enable Subscription Notifications"` + EnableTradeNotify *bool `gorm:"default:false;not null;comment:Enable Trade Notifications"` + AuthMethods []AuthMethods `gorm:"foreignKey:UserId;references:Id"` + UserDevices []Device `gorm:"foreignKey:UserId;references:Id"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (User) TableName() string { + return "user" +} + +type OldUser struct { + Id int64 `gorm:"primaryKey"` + Email string `gorm:"index:idx_email;type:varchar(100);comment:Email"` + //Telephone string `gorm:"index:idx_telephone;type:varchar(20);default:'';comment:Telephone"` + //TelephoneAreaCode string `gorm:"index:idx_telephone;type:varchar(20);default:'';comment:TelephoneAreaCode"` + Password string `gorm:"type:varchar(100);not null;comment:User Password"` + Avatar string `gorm:"type:varchar(200);default:'';comment:User Avatar"` + Balance int64 `gorm:"default:0;comment:User Balance"` // User Balance Amount + Telegram int64 `gorm:"default:null;comment:Telegram Account"` + ReferCode string `gorm:"type:varchar(20);default:'';comment:Referral Code"` + RefererId int64 `gorm:"index:idx_referer;comment:Referrer ID"` + Commission int64 `gorm:"default:0;comment:Commission"` // Commission Amount + GiftAmount int64 `gorm:"default:0;comment:User Gift Amount"` + Enable *bool `gorm:"default:true;not null;comment:Is Account Enabled"` + IsAdmin *bool `gorm:"default:false;not null;comment:Is Admin"` + ValidEmail *bool `gorm:"default:false;not null;comment:Is Email Verified"` + EnableEmailNotify *bool `gorm:"default:false;not null;comment:Enable Email Notifications"` + EnableTelegramNotify *bool `gorm:"default:false;not null;comment:Enable Telegram Notifications"` + EnableBalanceNotify *bool `gorm:"default:false;not null;comment:Enable Balance Change Notifications"` + EnableLoginNotify *bool `gorm:"default:false;not null;comment:Enable Login Notifications"` + EnableSubscribeNotify *bool `gorm:"default:false;not null;comment:Enable Subscription Notifications"` + EnableTradeNotify *bool `gorm:"default:false;not null;comment:Enable Trade Notifications"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` + DeletedAt gorm.DeletedAt `gorm:"default:null;comment:Deletion Time"` + IsDel soft_delete.DeletedAt `gorm:"softDelete:flag,DeletedAtField:DeletedAt;comment:1: Normal 0: Deleted"` // Using `1` and `0` to indicate +} + +func (OldUser) TableName() string { + return "user" +} + +type Subscribe struct { + Id int64 `gorm:"primaryKey"` + UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` + User User `gorm:"foreignKey:UserId;references:Id"` + OrderId int64 `gorm:"index:idx_order_id;not null;comment:Order ID"` + SubscribeId int64 `gorm:"index:idx_subscribe_id;not null;comment:Subscription ID"` + StartTime time.Time `gorm:"default:CURRENT_TIMESTAMP(3);not null;comment:Subscription Start Time"` + ExpireTime time.Time `gorm:"default:NULL;comment:Subscription Expire Time"` + FinishedAt time.Time `gorm:"default:NULL;comment:Finished Time"` + Traffic int64 `gorm:"default:0;comment:Traffic"` + Download int64 `gorm:"default:0;comment:Download Traffic"` + Upload int64 `gorm:"default:0;comment:Upload Traffic"` + Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"` + UUID string `gorm:"type:varchar(255);unique;index:idx_uuid;default:'';comment:UUID"` + Status uint8 `gorm:"type:tinyint(1);default:0;comment:Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired 4: Deducted"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (Subscribe) TableName() string { + return "user_subscribe" +} + +type BalanceLog struct { + Id int64 `gorm:"primaryKey"` + UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` + Amount int64 `gorm:"not null;comment:Amount"` + Type uint8 `gorm:"type:tinyint(1);not null;comment:Type: 1: Recharge 2: Withdraw 3: Payment 4: Refund 5: Reward"` + OrderId int64 `gorm:"default:null;comment:Order ID"` + Balance int64 `gorm:"not null;comment:Balance"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` +} + +func (BalanceLog) TableName() string { + return "user_balance_log" +} + +type GiftAmountLog struct { + Id int64 `gorm:"primaryKey"` + UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` + UserSubscribeId int64 `gorm:"default:null;comment:Deduction User Subscribe ID"` + OrderNo string `gorm:"default:null;comment:Order No."` + Type uint8 `gorm:"type:tinyint(1);not null;comment:Type: 1: Increase 2: Reduce"` + Amount int64 `gorm:"not null;comment:Amount"` + Balance int64 `gorm:"not null;comment:Balance"` + Remark string `gorm:"type:varchar(255);default:'';comment:Remark"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` +} + +func (GiftAmountLog) TableName() string { + return "user_gift_amount_log" +} + +type CommissionLog struct { + Id int64 `gorm:"primaryKey"` + UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` + OrderNo string `gorm:"default:null;comment:Order No."` + Amount int64 `gorm:"not null;comment:Amount"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` +} + +func (CommissionLog) TableName() string { + return "user_commission_log" +} + +type AuthMethods struct { + Id int64 `gorm:"primaryKey"` + UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` + AuthType string `gorm:"type:varchar(255);not null;comment:Auth Type 1: apple 2: google 3: github 4: facebook 5: telegram 6: email 7: mobile 8: device"` + AuthIdentifier string `gorm:"type:varchar(255);unique;index:idx_auth_identifier;not null;comment:Auth Identifier"` + Verified bool `gorm:"default:false;not null;comment:Is Verified"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (AuthMethods) TableName() string { + return "user_auth_methods" +} + +type Device struct { + Id int64 `gorm:"primaryKey"` + Ip string `gorm:"type:varchar(255);not null;comment:Device IP"` + UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` + UserAgent string `gorm:"default:null;comment:UserAgent."` + Identifier string `gorm:"type:varchar(255);unique;index:idx_identifier;default:'';comment:Device Identifier"` + Online bool `gorm:"default:false;not null;comment:Online"` + Enabled bool `gorm:"default:true;not null;comment:Enabled"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (Device) TableName() string { + return "user_device" +} + +type DeviceOnlineRecord struct { + Id int64 `gorm:"primaryKey"` + UserId int64 `gorm:"comment:User ID"` + Identifier string `gorm:"comment:Device Identifier"` + OnlineTime time.Time `gorm:"comment:Online Time"` // The time when the device goes online + OfflineTime time.Time `gorm:"comment:Offline Time"` + OnlineSeconds int64 `gorm:"comment:Offline Seconds"` + DurationDays int64 `gorm:"comment:Duration Days"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` +} + +func (DeviceOnlineRecord) TableName() string { + return "user_device_online_record" +} + +type LoginLog struct { + Id int64 `gorm:"primaryKey"` + UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` + LoginIP string `gorm:"type:varchar(255);not null;comment:Login IP"` + UserAgent string `gorm:"type:text;not null;comment:UserAgent"` + Success *bool `gorm:"default:false;not null;comment:Login Success"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` +} + +func (LoginLog) TableName() string { + return "user_login_log" +} + +type SubscribeLog struct { + Id int64 `gorm:"primaryKey"` + UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` + UserSubscribeId int64 `gorm:"index:idx_user_subscribe_id;not null;comment:User Subscribe ID"` + Token string `gorm:"type:varchar(255);not null;comment:Token"` + IP string `gorm:"type:varchar(255);not null;comment:IP"` + UserAgent string `gorm:"type:text;not null;comment:UserAgent"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` +} + +func (SubscribeLog) TableName() string { + return "user_subscribe_log" +} + +const ( + ResetSubscribeTypeAuto uint8 = 1 + ResetSubscribeTypeAdvance uint8 = 2 + ResetSubscribeTypePaid uint8 = 3 +) + +type FilterResetSubscribeLogParams struct { + Page int + Size int + Type uint8 + UserId int64 + OrderNo string + UserSubscribeId int64 +} + +type ResetSubscribeLog struct { + Id int64 `gorm:"primaryKey"` + UserId int64 `gorm:"type:bigint;index:idx_user_id;not null;comment:User ID"` + Type uint8 `gorm:"type:tinyint(1);not null;comment:Type: 1: Auto 2: Advance 3: Paid"` + OrderNo string `gorm:"type:varchar(255);default:null;comment:Order No."` + UserSubscribeId int64 `gorm:"type:bigint;index:idx_user_subscribe_id;not null;comment:User Subscribe ID"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` +} + +func (ResetSubscribeLog) TableName() string { + return "user_reset_subscribe_log" +} diff --git a/internal/server.go b/internal/server.go new file mode 100644 index 0000000..6f47675 --- /dev/null +++ b/internal/server.go @@ -0,0 +1,124 @@ +package internal + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net/http" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "github.com/perfect-panel/ppanel-server/pkg/proc" + "github.com/perfect-panel/ppanel-server/pkg/trace" + + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/redis" + "github.com/gin-gonic/gin" + "github.com/perfect-panel/ppanel-server/initialize" + "github.com/perfect-panel/ppanel-server/internal/handler" + "github.com/perfect-panel/ppanel-server/internal/middleware" + "github.com/perfect-panel/ppanel-server/internal/svc" +) + +type Service struct { + server *http.Server + svc *svc.ServiceContext +} + +func NewService(svc *svc.ServiceContext) *Service { + return &Service{ + svc: svc, + } +} + +func initServer(svc *svc.ServiceContext) *gin.Engine { + + // start init system config + initialize.StartInitSystemConfig(svc) + // init gin server + r := gin.Default() + r.RemoteIPHeaders = []string{"X-Original-Forwarded-For", "X-Forwarded-For", "X-Real-IP"} + // init session + sessionStore, err := redis.NewStore(10, "tcp", svc.Config.Redis.Host, svc.Config.Redis.Pass, []byte(svc.Config.JwtAuth.AccessSecret)) + if err != nil { + logger.Errorw("init session error", logger.Field("error", err.Error())) + panic(err) + } + r.Use(sessions.Sessions("ppanel", sessionStore)) + // use cors middleware + r.Use(middleware.TraceMiddleware(svc), middleware.LoggerMiddleware(svc), middleware.CorsMiddleware, middleware.PanDomainMiddleware(svc), gin.Recovery()) + + // register handlers + handler.RegisterHandlers(r, svc) + // register subscribe handler + handler.RegisterSubscribeHandlers(r, svc) + // register telegram handler + handler.RegisterTelegramHandlers(r, svc) + // register notify handler + handler.RegisterNotifyHandlers(r, svc) + return r +} + +func (m *Service) Start() { + if m.svc == nil { + panic("config file path is nil") + } + // init service + r := initServer(m.svc) + serverAddr := fmt.Sprintf("%v:%d", m.svc.Config.Host, m.svc.Config.Port) + m.server = &http.Server{ + Addr: serverAddr, + Handler: r, + TLSConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + }, + } + trace.StartAgent(trace.Config{ + Name: "ppanel", + Sampler: 1.0, + Batcher: "", + }) + proc.AddShutdownListener(func() { + trace.StopAgent() + }) + m.svc.Restart = m.Restart + logger.Infof("server start at %v", serverAddr) + if m.svc.Config.TLS.Enable { + if err := m.server.ListenAndServeTLS(m.svc.Config.TLS.CertFile, m.svc.Config.TLS.KeyFile); err != nil && !errors.Is(err, http.ErrServerClosed) { + logger.Errorf("server start error: %s", err.Error()) + } + } else { + if err := m.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + logger.Errorf("server start error: %s", err.Error()) + } + } +} + +func (m *Service) Stop() { + if m.server == nil { + return + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := m.server.Shutdown(ctx); err != nil { + logger.Errorf("server shutdown error: %s", err.Error()) + } + logger.Info("server shutdown") +} + +func (m *Service) Restart() error { + if m.server == nil { + return errors.New("server is nil") + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := m.server.Shutdown(ctx); err != nil { + logger.Errorf("server shutdown error: %v", err.Error()) + return err + } + logger.Info("server shutdown") + go m.Start() + return nil +} diff --git a/internal/svc/asynq.go b/internal/svc/asynq.go new file mode 100644 index 0000000..07a3003 --- /dev/null +++ b/internal/svc/asynq.go @@ -0,0 +1,10 @@ +package svc + +import ( + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/config" +) + +func NewAsynqClient(c config.Config) *asynq.Client { + return asynq.NewClient(asynq.RedisClientOpt{Addr: c.Redis.Host, Password: c.Redis.Pass, DB: 5}) +} diff --git a/internal/svc/devce.go b/internal/svc/devce.go new file mode 100644 index 0000000..87b6668 --- /dev/null +++ b/internal/svc/devce.go @@ -0,0 +1,129 @@ +package svc + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/model/user" + + "github.com/perfect-panel/ppanel-server/pkg/device" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +func NewDeviceManager(srv *ServiceContext) *device.DeviceManager { + ctx := context.Background() + manager := device.NewDeviceManager(30, 30) + + //设备离线处理 + manager.OnDeviceOffline = func(userID int64, deviceID, session string, createAt time.Time) { + oneDevice, err := srv.UserModel.FindOneDeviceByIdentifier(ctx, deviceID) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + logger.Errorw("failed to find device", logger.Field("error", err.Error()), logger.Field("device_id", deviceID)) + } + return + } + + //更新设备状态为离线 + oneDevice.Online = false + err = srv.UserModel.UpdateDevice(ctx, oneDevice) + if err != nil { + logger.Errorw("[DeviceManager] failed to update device", logger.Field("error", err.Error()), logger.Field("device_id", deviceID)) + } + + //当前时间为设备离线时间 + currentTime := time.Now() + endTime := currentTime.Format("2006-01-02 00:00:00") + parseStart, _ := time.Parse(time.DateTime, endTime) + startTime := parseStart.Add(time.Hour * 24).Format(time.DateTime) + deviceOnlineRecord := user.DeviceOnlineRecord{ + UserId: userID, + Identifier: deviceID, + OnlineTime: createAt, + OfflineTime: currentTime, + OnlineSeconds: int64(currentTime.Sub(createAt).Seconds()), + } + + //获取设备昨日在线记录 + var onlineRecord user.DeviceOnlineRecord + if err := srv.DB.Model(&onlineRecord).Where("user_id = ? and create_at >= ? and create_at < ?", userID, startTime, endTime).First(&onlineRecord).Error; err != nil { + //昨日未在线,连续在线天数为1 + deviceOnlineRecord.DurationDays = 1 + } else { + //昨日在线,连续在线天数为,昨天连续在线天数+1,等于当前连续在线天数 + deviceOnlineRecord.DurationDays = onlineRecord.DurationDays + 1 + } + + if err := srv.DB.Create(&deviceOnlineRecord).Error; err != nil { + logger.Errorw("[DeviceOnlineRecord] failed to DeviceOnlineRecord", logger.Field("error", err.Error()), logger.Field("device_id", deviceID)) + } + } + + //设备上线处理 + manager.OnDeviceOnline = func(userID int64, deviceID, session string) { + oneDevice, err := srv.UserModel.FindOneDeviceByIdentifier(ctx, deviceID) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + logger.Errorw("failed to find device", logger.Field("error", err.Error()), logger.Field("device_id", deviceID)) + } + return + } + oneDevice.Online = true + err = srv.UserModel.UpdateDevice(ctx, oneDevice) + if err != nil { + logger.Errorw("[DeviceManager] failed to update device", logger.Field("error", err.Error()), logger.Field("device_id", deviceID)) + return + } + } + + manager.OnDeviceKicked = func(userID int64, deviceID, session string, operator device.Operator) { + //管理员踢下线 + if operator == device.Admin { + message := DeviceMessage{Method: DeviceKickedAdmin} + _ = manager.SendToDevice(userID, deviceID, message.Json()) + //将登陆凭证从缓存中删除 + srv.Redis.Del(ctx, fmt.Sprintf("%v:%v", config.SessionIdKey, session)) + return + } + + //登陆设备超过限制踢下线 + if operator == device.MaxDevices { + message := DeviceMessage{Method: DeviceKickedMax} + _ = manager.SendToDevice(userID, deviceID, message.Json()) + //将登陆凭证从缓存中删除 + srv.Redis.Del(ctx, fmt.Sprintf("%v:%v", config.SessionIdKey, session)) + return + } + + } + + manager.OnMessage = func(userID int64, deviceID, session string, message string) { + logger.Infof("userid: %d ,device_number: %s,session: %s, message: %v", userID, deviceID, session, message) + } + return manager +} + +type DeviceMessage struct { + Method DeviceMessageMethod `json:"method"` +} + +func (dm *DeviceMessage) Json() string { + jsonData, _ := json.Marshal(dm) + return string(jsonData) +} + +type DeviceMessageMethod string + +const ( + // DeviceKickedMax 设备数量超出限制 + DeviceKickedMax DeviceMessageMethod = "kicked_device" + // DeviceKickedAdmin 管理员踢下线 + DeviceKickedAdmin DeviceMessageMethod = "kicked_admin" + // SubscribeUpdate 订阅有更新 + SubscribeUpdate DeviceMessageMethod = "subscribe_update" +) diff --git a/internal/svc/logger.go b/internal/svc/logger.go new file mode 100644 index 0000000..c5e1e94 --- /dev/null +++ b/internal/svc/logger.go @@ -0,0 +1,14 @@ +package svc + +import ( + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +func NewLogger(c config.Config) *logger.Logger { + //log := logger.New(c.Logger) + //// replace the default logger + //logger = log + //return log + return nil +} diff --git a/internal/svc/serviceContext.go b/internal/svc/serviceContext.go new file mode 100644 index 0000000..a6bbd1b --- /dev/null +++ b/internal/svc/serviceContext.go @@ -0,0 +1,111 @@ +package svc + +import ( + "context" + + "github.com/perfect-panel/ppanel-server/pkg/device" + + "github.com/perfect-panel/ppanel-server/internal/model/ads" + "github.com/perfect-panel/ppanel-server/internal/model/cache" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/model/announcement" + "github.com/perfect-panel/ppanel-server/internal/model/application" + "github.com/perfect-panel/ppanel-server/internal/model/auth" + "github.com/perfect-panel/ppanel-server/internal/model/coupon" + "github.com/perfect-panel/ppanel-server/internal/model/document" + "github.com/perfect-panel/ppanel-server/internal/model/log" + "github.com/perfect-panel/ppanel-server/internal/model/order" + "github.com/perfect-panel/ppanel-server/internal/model/payment" + "github.com/perfect-panel/ppanel-server/internal/model/server" + "github.com/perfect-panel/ppanel-server/internal/model/subscribe" + "github.com/perfect-panel/ppanel-server/internal/model/subscribeType" + "github.com/perfect-panel/ppanel-server/internal/model/system" + "github.com/perfect-panel/ppanel-server/internal/model/ticket" + "github.com/perfect-panel/ppanel-server/internal/model/traffic" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/pkg/limit" + "github.com/perfect-panel/ppanel-server/pkg/nodeMultiplier" + "github.com/perfect-panel/ppanel-server/pkg/orm" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +type ServiceContext struct { + DB *gorm.DB + Redis *redis.Client + Config config.Config + Queue *asynq.Client + NodeCache *cache.NodeCacheClient + AuthModel auth.Model + AdsModel ads.Model + LogModel log.Model + UserModel user.Model + OrderModel order.Model + TicketModel ticket.Model + ServerModel server.Model + SystemModel system.Model + CouponModel coupon.Model + PaymentModel payment.Model + DocumentModel document.Model + SubscribeModel subscribe.Model + TrafficLogModel traffic.Model + ApplicationModel application.Model + AnnouncementModel announcement.Model + SubscribeTypeModel subscribeType.Model + Restart func() error + TelegramBot *tgbotapi.BotAPI + NodeMultiplierManager *nodeMultiplier.Manager + AuthLimiter *limit.PeriodLimit + DeviceManager *device.DeviceManager +} + +func NewServiceContext(c config.Config) *ServiceContext { + // gorm initialize + db, err := orm.ConnectMysql(orm.Mysql{ + Config: c.MySQL, + }) + if err != nil { + panic(err.Error()) + } + rds := redis.NewClient(&redis.Options{ + Addr: c.Redis.Host, + Password: c.Redis.Pass, + DB: c.Redis.DB, + }) + err = rds.Ping(context.Background()).Err() + if err != nil { + panic(err.Error()) + } else { + _ = rds.FlushAll(context.Background()).Err() + } + authLimiter := limit.NewPeriodLimit(86400, 15, rds, config.SendCountLimitKeyPrefix, limit.Align()) + srv := &ServiceContext{ + DB: db, + Redis: rds, + Config: c, + Queue: NewAsynqClient(c), + NodeCache: cache.NewNodeCacheClient(rds), + AuthLimiter: authLimiter, + AdsModel: ads.NewModel(db, rds), + LogModel: log.NewModel(db), + AuthModel: auth.NewModel(db, rds), + UserModel: user.NewModel(db, rds), + OrderModel: order.NewModel(db, rds), + TicketModel: ticket.NewModel(db, rds), + ServerModel: server.NewModel(db, rds), + SystemModel: system.NewModel(db, rds), + CouponModel: coupon.NewModel(db, rds), + PaymentModel: payment.NewModel(db, rds), + DocumentModel: document.NewModel(db, rds), + SubscribeModel: subscribe.NewModel(db, rds), + TrafficLogModel: traffic.NewModel(db), + ApplicationModel: application.NewModel(db, rds), + AnnouncementModel: announcement.NewModel(db, rds), + } + srv.DeviceManager = NewDeviceManager(srv) + return srv + +} diff --git a/internal/svc/validate.go b/internal/svc/validate.go new file mode 100644 index 0000000..22df78d --- /dev/null +++ b/internal/svc/validate.go @@ -0,0 +1,33 @@ +package svc + +import ( + "reflect" + + "github.com/go-playground/locales/en" + ut "github.com/go-playground/universal-translator" + "github.com/go-playground/validator/v10" + enTranslations "github.com/go-playground/validator/v10/translations/en" + "github.com/pkg/errors" +) + +func (svc ServiceContext) Validate(dataStruct interface{}) error { + enUs := en.New() + validate := validator.New() + // RegisterTagNameFunc registers a function to get field name for error messages + validate.RegisterTagNameFunc(func(fld reflect.StructField) string { + name := fld.Tag.Get("label") + return name + }) + + uni := ut.New(enUs) + trans, _ := uni.GetTranslator("en") + // RegisterDefaultTranslations registers the default translations + _ = enTranslations.RegisterDefaultTranslations(validate, trans) + err := validate.Struct(dataStruct) + if err != nil { + for _, err := range err.(validator.ValidationErrors) { + return errors.New(err.Translate(trans)) + } + } + return nil +} diff --git a/internal/trace/trace.go b/internal/trace/trace.go new file mode 100644 index 0000000..3304b04 --- /dev/null +++ b/internal/trace/trace.go @@ -0,0 +1,25 @@ +package trace + +import ( + "context" + + "go.opentelemetry.io/otel/trace" +) + +func SpanIDFromContext(ctx context.Context) string { + spanCtx := trace.SpanContextFromContext(ctx) + if spanCtx.HasSpanID() { + return spanCtx.SpanID().String() + } + + return "" +} + +func TraceIDFromContext(ctx context.Context) string { + spanCtx := trace.SpanContextFromContext(ctx) + if spanCtx.HasTraceID() { + return spanCtx.TraceID().String() + } + + return "" +} diff --git a/internal/trace/trace_test.go b/internal/trace/trace_test.go new file mode 100644 index 0000000..e40339c --- /dev/null +++ b/internal/trace/trace_test.go @@ -0,0 +1,32 @@ +package trace + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" + oteltrace "go.opentelemetry.io/otel/trace" +) + +func TestSpanIDFromContext(t *testing.T) { + tracer := sdktrace.NewTracerProvider().Tracer("test") + ctx, span := tracer.Start( + context.Background(), + "foo", + oteltrace.WithSpanKind(oteltrace.SpanKindClient), + oteltrace.WithAttributes(semconv.HTTPClientAttributesFromHTTPRequest(httptest.NewRequest(http.MethodGet, "/", nil))...), + ) + defer span.End() + + assert.NotEmpty(t, TraceIDFromContext(ctx)) + assert.NotEmpty(t, SpanIDFromContext(ctx)) +} + +func TestSpanIDFromContextEmpty(t *testing.T) { + assert.Empty(t, TraceIDFromContext(context.Background())) + assert.Empty(t, SpanIDFromContext(context.Background())) +} diff --git a/internal/types/subscribe.go b/internal/types/subscribe.go new file mode 100644 index 0000000..0e8ab26 --- /dev/null +++ b/internal/types/subscribe.go @@ -0,0 +1,13 @@ +package types + +type ( + SubscribeRequest struct { + Flag string + Token string + UA string + } + SubscribeResponse struct { + Config []byte + Header string + } +) diff --git a/internal/types/types.go b/internal/types/types.go new file mode 100644 index 0000000..27a4d31 --- /dev/null +++ b/internal/types/types.go @@ -0,0 +1,2303 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.7.2 + +package types + +type Ads struct { + Id int `json:"id"` + Title string `json:"title"` + Type string `json:"type"` + Content string `json:"content"` + Description string `json:"description"` + TargetURL string `json:"target_url"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + Status int `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type AlipayNotifyResponse struct { + ReturnCode string `json:"return_code"` +} + +type Announcement struct { + Id int64 `json:"id"` + Title string `json:"title"` + Content string `json:"content"` + Show *bool `json:"show"` + Pinned *bool `json:"pinned"` + Popup *bool `json:"popup"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type AppAuthCheckRequest struct { + Method string `json:"method" validate:"required" validate:"required,oneof=device email mobile"` + Account string `json:"account"` + Identifier string `json:"identifier" validate:"required"` + UserAgent string `json:"user_agent" validate:"required,oneof=windows mac linux android ios harmony"` + AreaCode string `json:"area_code"` +} + +type AppAuthCheckResponse struct { + Status bool +} + +type AppAuthRequest struct { + Method string `json:"method" validate:"required" validate:"required,oneof=device email mobile"` + Account string `json:"account"` + Password string `json:"password"` + Identifier string `json:"identifier" validate:"required"` + UserAgent string `json:"user_agent" validate:"required,oneof=windows mac linux android ios harmony"` + Code string `json:"code"` + Invite string `json:"invite"` + AreaCode string `json:"area_code"` + CfToken string `json:"cf_token,optional"` +} + +type AppAuthRespone struct { + Token string `json:"token"` +} + +type AppConfigRequest struct { + UserAgent string `json:"user_agent" validate:"required,oneof=windows mac linux android ios harmony"` +} + +type AppConfigResponse struct { + EncryptionKey string `json:"encryption_key"` + EncryptionMethod string `json:"encryption_method"` + Domains []string `json:"domains"` + StartupPicture string `json:"startup_picture"` + StartupPictureSkipTime int64 `json:"startup_picture_skip_time"` + Application AppInfo `json:"applications"` + OfficialEmail string `json:"official_email"` + OfficialWebsite string `json:"official_website"` + OfficialTelegram string `json:"official_telegram"` + OfficialTelephone string `json:"official_telephone"` + InvitationLink string `json:"invitation_link"` + KrWebsiteId string `json:"kr_website_id"` +} + +type AppInfo struct { + Id int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Url string `json:"url"` + Version string `json:"version"` + VersionReview string `json:"version_review"` + VersionDescription string `json:"version_description"` + IsDefault bool `json:"is_default"` +} + +type AppRuleGroupListResponse struct { + Total int64 `json:"total"` + List []ServerRuleGroup `json:"list"` +} + +type AppSendCodeRequest struct { + Method string `json:"method" validate:"required" validate:"required,oneof=email mobile"` + Account string `json:"account"` + AreaCode string `json:"area_code"` + CfToken string `json:"cf_token,optional"` +} + +type AppSendCodeRespone struct { + Status bool `json:"status"` + Code string `json:"code,omitempty"` +} + +type AppUserSubcbribe struct { + Id int64 `json:"id"` + Name string `json:"name"` + Upload int64 `json:"upload"` + Traffic int64 `json:"traffic"` + Download int64 `json:"download"` + DeviceLimit int64 `json:"device_limit"` + StartTime string `json:"start_time"` + ExpireTime string `json:"expire_time"` + List []AppUserSubscbribeNode `json:"list"` +} + +type AppUserSubscbribeNode struct { + Id int64 `json:"id"` + Name string `json:"name"` + Uuid string `json:"uuid"` + Protocol string `json:"protocol"` + RelayMode string `json:"relay_mode"` + RelayNode string `json:"relay_node"` + ServerAddr string `json:"server_addr"` + SpeedLimit int `json:"speed_limit"` + Tags []string `json:"tags"` + Traffic int64 `json:"traffic"` + TrafficRatio float64 `json:"traffic_ratio"` + Upload int64 `json:"upload"` + Config string `json:"config"` + Country string `json:"country"` + City string `json:"city"` + Latitude string `json:"latitude"` + Longitude string `json:"longitude"` + LatitudeCountry string `json:"latitudeCountry"` + LongitudeCountry string `json:"longitudeCountry"` + CreatedAt int64 `json:"created_at"` + Download int64 `json:"download"` +} + +type AppUserSubscbribeNodeRequest struct { + Id int64 `form:"id" validate:"required"` +} + +type AppUserSubscbribeNodeResponse struct { + List []AppUserSubscbribeNode `json:"list"` +} + +type AppUserSubscbribeResponse struct { + List []AppUserSubcbribe `json:"list"` +} + +type AppUserSubscribeRequest struct { + ContainsNodes *bool `form:"contains_nodes"` +} + +type AppleLoginCallbackRequest struct { + Code string `form:"code"` + IDToken string `form:"id_token"` + State string `form:"state"` +} + +type Application struct { + Id int64 `json:"id"` + Icon string `json:"icon"` + Name string `json:"name"` + Description string `json:"description"` + SubscribeType string `json:"subscribe_type"` +} + +type ApplicationConfig struct { + AppId int64 `json:"app_id"` + EncryptionKey string `json:"encryption_key"` + EncryptionMethod string `json:"encryption_method"` + Domains []string `json:"domains" validate:"required"` + StartupPicture string `json:"startup_picture"` + StartupPictureSkipTime int64 `json:"startup_picture_skip_time"` +} + +type ApplicationPlatform struct { + IOS []*ApplicationVersion `json:"ios,omitempty"` + MacOS []*ApplicationVersion `json:"macos,omitempty"` + Linux []*ApplicationVersion `json:"linux,omitempty"` + Android []*ApplicationVersion `json:"android,omitempty"` + Windows []*ApplicationVersion `json:"windows,omitempty"` + Harmony []*ApplicationVersion `json:"harmony,omitempty"` +} + +type ApplicationResponse struct { + Applications []ApplicationResponseInfo `json:"applications"` +} + +type ApplicationResponseInfo struct { + Id int64 `json:"id"` + Name string `json:"name"` + Icon string `json:"icon"` + Description string `json:"description"` + SubscribeType string `json:"subscribe_type"` + Platform ApplicationPlatform `json:"platform"` +} + +type ApplicationVersion struct { + Id int64 `json:"id"` + Url string `json:"url"` + Version string `json:"version" validate:"required"` + Description string `json:"description"` + IsDefault bool `json:"is_default"` +} + +type AuthConfig struct { + Mobile MobileAuthenticateConfig `json:"mobile"` + Email EmailAuthticateConfig `json:"email"` + Register PubilcRegisterConfig `json:"register"` +} + +type AuthMethodConfig struct { + Id int64 `json:"id"` + Method string `json:"method"` + Config interface{} `json:"config"` + Enabled bool `json:"enabled"` +} + +type BatchDeleteCouponRequest struct { + Ids []int64 `json:"ids" validate:"required"` +} + +type BatchDeleteDocumentRequest struct { + Ids []int64 `json:"ids" validate:"required"` +} + +type BatchDeleteNodeGroupRequest struct { + Ids []int64 `json:"ids" validate:"required"` +} + +type BatchDeleteNodeRequest struct { + Ids []int64 `json:"ids" validate:"required"` +} + +type BatchDeleteSubscribeGroupRequest struct { + Ids []int64 `json:"ids" validate:"required"` +} + +type BatchDeleteSubscribeRequest struct { + Ids []int64 `json:"ids" validate:"required"` +} + +type BatchDeleteUserRequest struct { + Ids []int64 `json:"ids" validate:"required"` +} + +type BindOAuthCallbackRequest struct { + Method string `json:"method"` + Callback interface{} `json:"callback"` +} + +type BindOAuthRequest struct { + Method string `json:"method"` + Redirect string `json:"redirect"` +} + +type BindOAuthResponse struct { + Redirect string `json:"redirect"` +} + +type BindTelegramResponse struct { + Url string `json:"url"` + ExpiredAt int64 `json:"expired_at"` +} + +type CheckUserRequest struct { + Email string `form:"email" validate:"required"` +} + +type CheckUserResponse struct { + Exist bool `json:"exist"` +} + +type CheckVerificationCodeRequest struct { + Method string `json:"method" validate:"required,oneof=email mobile"` + Account string `json:"account" validate:"required"` + Code string `json:"code" validate:"required"` + Type uint8 `json:"type" validate:"required"` +} + +type CheckVerificationCodeRespone struct { + Status bool `json:"status"` +} + +type CheckoutOrderRequest struct { + OrderNo string `json:"orderNo"` + ReturnUrl string `json:"returnUrl,omitempty"` +} + +type CheckoutOrderResponse struct { + Type string `json:"type"` + CheckoutUrl string `json:"checkout_url,omitempty"` + Stripe *StripePayment `json:"stripe,omitempty"` +} + +type CloseOrderRequest struct { + OrderNo string `json:"orderNo" validate:"required"` +} + +type CommissionLog struct { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + OrderNo string `json:"order_no"` + Amount int64 `json:"amount"` + CreatedAt int64 `json:"created_at"` +} + +type ConnectionRecords struct { + CurrentContinuousDays int64 `json:"current_continuous_days"` + HistoryContinuousDays int64 `json:"history_continuous_days"` + LongestSingleConnection int64 `json:"longest_single_connection"` +} + +type Coupon struct { + Id int64 `json:"id"` + Name string `json:"name"` + Code string `json:"code"` + Count int64 `json:"count"` + Type uint8 `json:"type"` + Discount int64 `json:"discount"` + StartTime int64 `json:"start_time"` + ExpireTime int64 `json:"expire_time"` + UserLimit int64 `json:"user_limit"` + Subscribe []int64 `json:"subscribe"` + UsedCount int64 `json:"used_count"` + Enable bool `json:"enable"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type CreateAdsRequest struct { + Title string `json:"title"` + Type string `json:"type"` + Content string `json:"content"` + Description string `json:"description"` + TargetURL string `json:"target_url"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + Status int `json:"status"` +} + +type CreateAnnouncementRequest struct { + Title string `json:"title" validate:"required"` + Content string `json:"content" validate:"required"` +} + +type CreateApplicationRequest struct { + Icon string `json:"icon"` + Name string `json:"name"` + Description string `json:"description"` + SubscribeType string `json:"subscribe_type"` + Platform ApplicationPlatform `json:"platform"` +} + +type CreateApplicationVersionRequest struct { + Url string `json:"url"` + Version string `json:"version" validate:"required"` + Description string `json:"description"` + Platform string `json:"platform" validate:"required,oneof=windows mac linux android ios harmony"` + IsDefault bool `json:"is_default"` + ApplicationId int64 `json:"application_id" validate:"required"` +} + +type CreateCouponRequest struct { + Name string `json:"name" validate:"required"` + Code string `json:"code,omitempty"` + Count int64 `json:"count,omitempty"` + Type uint8 `json:"type" validate:"required"` + Discount int64 `json:"discount" validate:"required"` + StartTime int64 `json:"start_time" validate:"required"` + ExpireTime int64 `json:"expire_time" validate:"required"` + UserLimit int64 `json:"user_limit,omitempty"` + Subscribe []int64 `json:"subscribe,omitempty"` + UsedCount int64 `json:"used_count,omitempty"` + Enable *bool `json:"enable,omitempty"` +} + +type CreateDocumentRequest struct { + Title string `json:"title" validate:"required"` + Content string `json:"content" validate:"required"` + Tags []string `json:"tags,omitempty" ` + Show *bool `json:"show"` +} + +type CreateNodeGroupRequest struct { + Name string `json:"name" validate:"required"` + Description string `json:"description"` +} + +type CreateNodeRequest struct { + Name string `json:"name" validate:"required"` + Tags []string `json:"tags"` + Country string `json:"country"` + City string `json:"city"` + ServerAddr string `json:"server_addr" validate:"required"` + RelayMode string `json:"relay_mode"` + RelayNode []NodeRelay `json:"relay_node"` + SpeedLimit int `json:"speed_limit"` + TrafficRatio float32 `json:"traffic_ratio"` + GroupId int64 `json:"group_id"` + Protocol string `json:"protocol" validate:"required"` + Config interface{} `json:"config" validate:"required"` + Enable *bool `json:"enable"` + Sort int64 `json:"sort"` +} + +type CreateOrderRequest struct { + UserId int64 `json:"user_id" validate:"required"` + Type uint8 `json:"type" validate:"required"` + Quantity int64 `json:"quantity,omitempty"` + Price int64 `json:"price" validate:"required"` + Amount int64 `json:"amount" validate:"required"` + Discount int64 `json:"discount,omitempty"` + Coupon string `json:"coupon,omitempty"` + CouponDiscount int64 `json:"coupon_discount,omitempty"` + Commission int64 `json:"commission"` + FeeAmount int64 `json:"fee_amount" validate:"required"` + PaymentId int64 `json:"payment_id" validate:"required"` + TradeNo string `json:"trade_no,omitempty"` + Status uint8 `json:"status,omitempty"` + SubscribeId int64 `json:"subscribe_id,omitempty"` +} + +type CreatePaymentMethodRequest struct { + Name string `json:"name" validate:"required"` + Platform string `json:"platform" validate:"required"` + Description string `json:"description"` + Icon string `json:"icon,omitempty"` + Domain string `json:"domain,omitempty"` + Config interface{} `json:"config" validate:"required"` + FeeMode uint `json:"fee_mode"` + FeePercent int64 `json:"fee_percent,omitempty"` + FeeAmount int64 `json:"fee_amount,omitempty"` + Enable *bool `json:"enable" validate:"required"` +} + +type CreateRuleGroupRequest struct { + Name string `json:"name" validate:"required"` + Icon string `json:"icon"` + Tags []string `json:"tags"` + Rules string `json:"rules"` + Enable bool `json:"enable"` +} + +type CreateSubscribeGroupRequest struct { + Name string `json:"name" validate:"required"` + Description string `json:"description"` +} + +type CreateSubscribeRequest struct { + Name string `json:"name" validate:"required"` + Description string `json:"description"` + UnitPrice int64 `json:"unit_price"` + UnitTime string `json:"unit_time"` + Discount []SubscribeDiscount `json:"discount"` + Replacement int64 `json:"replacement"` + Inventory int64 `json:"inventory"` + Traffic int64 `json:"traffic"` + SpeedLimit int64 `json:"speed_limit"` + DeviceLimit int64 `json:"device_limit"` + Quota int64 `json:"quota"` + GroupId int64 `json:"group_id"` + ServerGroup []int64 `json:"server_group"` + Server []int64 `json:"server"` + Show *bool `json:"show"` + Sell *bool `json:"sell"` + DeductionRatio int64 `json:"deduction_ratio"` + AllowDeduction *bool `json:"allow_deduction"` + ResetCycle int64 `json:"reset_cycle"` + RenewalReset *bool `json:"renewal_reset"` +} + +type CreateTicketFollowRequest struct { + TicketId int64 `json:"ticket_id" validate:"required"` + From string `json:"from" validate:"required"` + Type uint8 `json:"type" validate:"required"` + Content string `json:"content" validate:"required"` +} + +type CreateUserAuthMethodRequest struct { + UserId int64 `json:"user_id"` + AuthType string `json:"auth_type"` + AuthIdentifier string `json:"auth_identifier"` +} + +type CreateUserRequest struct { + Email string `json:"email"` + Telephone string `json:"telephone"` + TelephoneAreaCode string `json:"telephone_area_code"` + Password string `json:"password"` + ProductId int64 `json:"product_id"` + Duration int64 `json:"duration"` + RefererUser string `json:"referer_user"` + ReferCode string `json:"refer_code"` + Balance int64 `json:"balance"` + Commission int64 `json:"commission"` + GiftAmount int64 `json:"gift_amount"` + IsAdmin bool `json:"is_admin"` +} + +type CreateUserSubscribeRequest struct { + UserId int64 `json:"user_id"` + ExpiredAt int64 `json:"expired_at"` + Traffic int64 `json:"traffic"` + SubscribeId int64 `json:"subscribe_id"` +} + +type CreateUserTicketFollowRequest struct { + TicketId int64 `json:"ticket_id"` + From string `json:"from"` + Type uint8 `json:"type"` + Content string `json:"content"` +} + +type CreateUserTicketRequest struct { + Title string `json:"title"` + Description string `json:"description"` +} + +type Currency struct { + CurrencyUnit string `json:"currency_unit"` + CurrencySymbol string `json:"currency_symbol"` +} + +type CurrencyConfig struct { + AccessKey string `json:"access_key"` + CurrencyUnit string `json:"currency_unit"` + CurrencySymbol string `json:"currency_symbol"` +} + +type DeleteAccountRequest struct { + Method string `json:"method" validate:"required" validate:"required,oneof=email telephone device"` + Code string `json:"code"` +} + +type DeleteAdsRequest struct { + Id int64 `json:"id"` +} + +type DeleteAnnouncementRequest struct { + Id int64 `json:"id" validate:"required"` +} + +type DeleteApplicationRequest struct { + Id int64 `json:"id" validate:"required"` +} + +type DeleteApplicationVersionRequest struct { + Id int64 `json:"id" validate:"required"` +} + +type DeleteCouponRequest struct { + Id int64 `json:"id" validate:"required"` +} + +type DeleteDocumentRequest struct { + Id int64 `json:"id" validate:"required"` +} + +type DeleteNodeGroupRequest struct { + Id int64 `json:"id" validate:"required"` +} + +type DeleteNodeRequest struct { + Id int64 `json:"id" validate:"required"` +} + +type DeletePaymentMethodRequest struct { + Id int64 `json:"id" validate:"required"` +} + +type DeleteRuleGroupRequest struct { + Id int64 `json:"id" validate:"required"` +} + +type DeleteSubscribeGroupRequest struct { + Id int64 `json:"id" validate:"required"` +} + +type DeleteSubscribeRequest struct { + Id int64 `json:"id" validate:"required"` +} + +type DeleteUserAuthMethodRequest struct { + UserId int64 `json:"user_id"` + AuthType string `json:"auth_type"` +} + +type DeleteUserDeivceRequest struct { + Id int64 `json:"id"` +} + +type DeleteUserSubscribeRequest struct { + UserSubscribeId int64 `json:"user_subscribe_id"` +} + +type Document struct { + Id int64 `json:"id"` + Title string `json:"title"` + Content string `json:"content"` + Tags []string `json:"tags"` + Show bool `json:"show"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type EPayNotifyRequest struct { + Pid int64 `json:"pid" form:"pid"` + TradeNo string `json:"trade_no" form:"trade_no"` + OutTradeNo string `json:"out_trade_no" form:"out_trade_no"` + Type string `json:"type" form:"type"` + Name string `json:"name" form:"name"` + Money string `json:"money" form:"money"` + TradeStatus string `json:"trade_status" form:"trade_status"` + Param string `json:"param" form:"param"` + Sign string `json:"sign" form:"sign"` + SignType string `json:"sign_type" form:"sign_type"` +} + +type EmailAuthticateConfig struct { + Enable bool `json:"enable"` + EnableVerify bool `json:"enable_verify"` + EnableDomainSuffix bool `json:"enable_domain_suffix"` + DomainSuffixList string `json:"domain_suffix_list"` +} + +type Follow struct { + Id int64 `json:"id"` + TicketId int64 `json:"ticket_id"` + From string `json:"from"` + Type uint8 `json:"type"` + Content string `json:"content"` + CreatedAt int64 `json:"created_at"` +} + +type GetAdsDetailRequest struct { + Id int64 `form:"id"` +} + +type GetAdsListRequest struct { + Page int `form:"page"` + Size int `form:"size"` + Status *int `form:"status,omitempty"` + Search string `form:"search,omitempty"` +} + +type GetAdsListResponse struct { + Total int64 `json:"total"` + List []Ads `json:"list"` +} + +type GetAdsRequest struct { + Device string `form:"device"` + Position string `form:"position"` +} + +type GetAdsResponse struct { + List []Ads `json:"list"` +} + +type GetAnnouncementListRequest struct { + Page int64 `form:"page"` + Size int64 `form:"size"` + Show *bool `form:"show,omitempty"` + Pinned *bool `form:"pinned,omitempty"` + Popup *bool `form:"popup,omitempty"` + Search string `form:"search,omitempty"` +} + +type GetAnnouncementListResponse struct { + Total int64 `json:"total"` + List []Announcement `json:"list"` +} + +type GetAnnouncementRequest struct { + Id int64 `form:"id" validate:"required"` +} + +type GetAppcationResponse struct { + Config ApplicationConfig `json:"config"` + Applications []ApplicationResponseInfo `json:"applications"` +} + +type GetAuthMethodConfigRequest struct { + Method string `form:"method"` +} + +type GetAuthMethodListResponse struct { + List []AuthMethodConfig `json:"list"` +} + +type GetAvailablePaymentMethodsResponse struct { + List []PaymentMethod `json:"list"` +} + +type GetCouponListRequest struct { + Page int64 `form:"page" validate:"required"` + Size int64 `form:"size" validate:"required"` + Subscribe int64 `form:"subscribe,omitempty"` + Search string `form:"search,omitempty"` +} + +type GetCouponListResponse struct { + Total int64 `json:"total"` + List []Coupon `json:"list"` +} + +type GetDetailRequest struct { + Id int64 `form:"id" validate:"required"` +} + +type GetDocumentDetailRequest struct { + Id int64 `json:"id" validate:"required"` +} + +type GetDocumentListRequest struct { + Page int64 `form:"page" validate:"required"` + Size int64 `form:"size" validate:"required"` + Tag string `form:"tag,omitempty"` + Search string `form:"search,omitempty"` +} + +type GetDocumentListResponse struct { + Total int64 `json:"total"` + List []Document `json:"list"` +} + +type GetGlobalConfigResponse struct { + Site SiteConfig `json:"site"` + Verify VeifyConfig `json:"verify"` + Auth AuthConfig `json:"auth"` + Invite InviteConfig `json:"invite"` + Currency Currency `json:"currency"` + Subscribe SubscribeConfig `json:"subscribe"` + VerifyCode PubilcVerifyCodeConfig `json:"verify_code"` + OAuthMethods []string `json:"oauth_methods"` + WebAd bool `json:"web_ad"` +} + +type GetLoginLogRequest struct { + Page int `form:"page"` + Size int `form:"size"` +} + +type GetLoginLogResponse struct { + List []UserLoginLog `json:"list"` + Total int64 `json:"total"` +} + +type GetMessageLogListRequest struct { + Page int `form:"page"` + Size int `form:"size"` + Type string `form:"type"` + Platform string `form:"platform,omitempty"` + To string `form:"to,omitempty"` + Subject string `form:"subject,omitempty"` + Content string `form:"content,omitempty"` + Status int `form:"status,omitempty"` +} + +type GetMessageLogListResponse struct { + Total int64 `json:"total"` + List []MessageLog `json:"list"` +} + +type GetNodeDetailRequest struct { + Id int64 `form:"id" validate:"required"` +} + +type GetNodeGroupListResponse struct { + Total int64 `json:"total"` + List []ServerGroup `json:"list"` +} + +type GetNodeMultiplierResponse struct { + Periods []TimePeriod `json:"periods"` +} + +type GetNodeServerListRequest struct { + Page int `form:"page" validate:"required"` + Size int `form:"size" validate:"required"` + Tag string `form:"tag,omitempty"` + GroupId int64 `form:"group_id,omitempty"` + Search string `form:"search,omitempty"` +} + +type GetNodeServerListResponse struct { + Total int64 `json:"total"` + List []Server `json:"list"` +} + +type GetNodeTagListResponse struct { + Tags []string `json:"tags"` +} + +type GetOAuthMethodsResponse struct { + Methods []UserAuthMethod `json:"methods"` +} + +type GetOrderListRequest struct { + Page int64 `form:"page" validate:"required"` + Size int64 `form:"size" validate:"required"` + UserId int64 `form:"user_id,omitempty"` + Status uint8 `form:"status,omitempty"` + SubscribeId int64 `form:"subscribe_id,omitempty"` + Search string `form:"search,omitempty"` +} + +type GetOrderListResponse struct { + Total int64 `json:"total"` + List []Order `json:"list"` +} + +type GetPaymentMethodListRequest struct { + Page int `form:"page"` + Size int `form:"size"` + Platform string `form:"platform,omitempty"` + Search string `form:"search,omitempty"` + Enable *bool `form:"enable,omitempty"` +} + +type GetPaymentMethodListResponse struct { + Total int64 `json:"total"` + List []PaymentMethodDetail `json:"list"` +} + +type GetRuleGroupResponse struct { + Total int64 `json:"total"` + List []ServerRuleGroup `json:"list"` +} + +type GetServerConfigRequest struct { + ServerCommon +} + +type GetServerConfigResponse struct { + Basic ServerBasic `json:"basic"` + Protocol string `json:"protocol"` + Config interface{} `json:"config"` +} + +type GetServerUserListRequest struct { + ServerCommon +} + +type GetServerUserListResponse struct { + Users []ServerUser `json:"users"` +} + +type GetStatResponse struct { + User int64 `json:"user"` + Node int64 `json:"node"` + Country int64 `json:"country"` + Protocol []string `json:"protocol"` +} + +type GetSubscribeDetailsRequest struct { + Id int64 `form:"id" validate:"required"` +} + +type GetSubscribeGroupListResponse struct { + List []SubscribeGroup `json:"list"` + Total int64 `json:"total"` +} + +type GetSubscribeListRequest struct { + Page int64 `form:"page" validate:"required"` + Size int64 `form:"size" validate:"required"` + GroupId int64 `form:"group_id,omitempty"` + Search string `form:"search,omitempty"` +} + +type GetSubscribeListResponse struct { + List []SubscribeItem `json:"list"` + Total int64 `json:"total"` +} + +type GetSubscribeLogRequest struct { + Page int `form:"page"` + Size int `form:"size"` +} + +type GetSubscribeLogResponse struct { + List []UserSubscribeLog `json:"list"` + Total int64 `json:"total"` +} + +type GetSubscriptionResponse struct { + List []Subscribe `json:"list"` +} + +type GetTicketListRequest struct { + Page int64 `form:"page"` + Size int64 `form:"size"` + UserId int64 `form:"user_id,omitempty"` + Status *uint8 `form:"status,omitempty"` + Search string `form:"search,omitempty"` +} + +type GetTicketListResponse struct { + Total int64 `json:"total"` + List []Ticket `json:"list"` +} + +type GetTicketRequest struct { + Id int64 `form:"id" validate:"required"` +} + +type GetTosResponse struct { + TosContent string `json:"tos_content"` +} + +type GetUserAuthMethodRequest struct { + UserId int64 `json:"user_id"` +} + +type GetUserAuthMethodResponse struct { + AuthMethods []UserAuthMethod `json:"auth_methods"` +} + +type GetUserListRequest struct { + Page int `form:"page"` + Size int `form:"size"` + Search string `form:"search,omitempty"` + UserId *int64 `form:"user_id,omitempty"` + SubscribeId *int64 `form:"subscribe_id,omitempty"` + UserSubscribeId *int64 `form:"user_subscribe_id,omitempty"` +} + +type GetUserListResponse struct { + Total int64 `json:"total"` + List []User `json:"list"` +} + +type GetUserLoginLogsRequest struct { + Page int `form:"page"` + Size int `form:"size"` + UserId int64 `form:"user_id"` +} + +type GetUserLoginLogsResponse struct { + List []UserLoginLog `json:"list"` + Total int64 `json:"total"` +} + +type GetUserOnlineTimeStatisticsResponse struct { + WeeklyStats []WeeklyStat `json:"weekly_stats"` + ConnectionRecords ConnectionRecords `json:"connection_records"` +} + +type GetUserSubscribeByIdRequest struct { + Id int64 `form:"id" validate:"required"` +} + +type GetUserSubscribeDevicesRequest struct { + Page int `form:"page"` + Size int `form:"size"` + UserId int64 `form:"user_id"` + SubscribeId int64 `form:"subscribe_id"` +} + +type GetUserSubscribeDevicesResponse struct { + List []UserDevice `json:"list"` + Total int64 `json:"total"` +} + +type GetUserSubscribeListRequest struct { + Page int `form:"page"` + Size int `form:"size"` + UserId int64 `form:"user_id"` +} + +type GetUserSubscribeListResponse struct { + List []UserSubscribe `json:"list"` + Total int64 `json:"total"` +} + +type GetUserSubscribeLogsRequest struct { + Page int `form:"page"` + Size int `form:"size"` + UserId int64 `form:"user_id"` + SubscribeId int64 `form:"subscribe_id,omitempty"` +} + +type GetUserSubscribeLogsResponse struct { + List []UserSubscribeLog `json:"list"` + Total int64 `json:"total"` +} + +type GetUserSubscribeTrafficLogsRequest struct { + Page int `form:"page"` + Size int `form:"size"` + UserId int64 `form:"user_id"` + SubscribeId int64 `form:"subscribe_id"` + StartTime int64 `form:"start_time"` + EndTime int64 `form:"end_time"` +} + +type GetUserSubscribeTrafficLogsResponse struct { + List []TrafficLog `json:"list"` + Total int64 `json:"total"` +} + +type GetUserTicketDetailRequest struct { + Id int64 `form:"id" validate:"required"` +} + +type GetUserTicketListRequest struct { + Page int `form:"page"` + Size int `form:"size"` + Status *uint8 `form:"status,omitempty"` + Search string `form:"search,omitempty"` +} + +type GetUserTicketListResponse struct { + Total int64 `json:"total"` + List []Ticket `json:"list"` +} + +type GoogleLoginCallbackRequest struct { + Code string `form:"code"` + State string `form:"state"` +} + +type Hysteria2 struct { + Port int `json:"port" validate:"required"` + HopPorts string `json:"hop_ports" validate:"required"` + HopInterval int `json:"hop_interval" validate:"required"` + ObfsPassword string `json:"obfs_password" validate:"required"` + SecurityConfig SecurityConfig `json:"security_config"` +} + +type InviteConfig struct { + ForcedInvite bool `json:"forced_invite"` + ReferralPercentage int64 `json:"referral_percentage"` + OnlyFirstPurchase bool `json:"only_first_purchase"` +} + +type KickOfflineRequest struct { + Id int64 `json:"id"` +} + +type LogResponse struct { + List interface{} `json:"list"` +} + +type LoginResponse struct { + Token string `json:"token"` +} + +type MessageLog struct { + Id int64 `json:"id"` + Type string `json:"type"` + Platform string `json:"platform"` + To string `json:"to"` + Subject string `json:"subject"` + Content string `json:"content"` + Status int `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type MobileAuthenticateConfig struct { + Enable bool `json:"enable"` + EnableWhitelist bool `json:"enable_whitelist"` + Whitelist []string `json:"whitelist"` +} + +type NodeConfig struct { + NodeSecret string `json:"node_secret"` + NodePullInterval int64 `json:"node_pull_interval"` + NodePushInterval int64 `json:"node_push_interval"` +} + +type NodeRelay struct { + Host string `json:"host"` + Port int `json:"port"` + Prefix string `json:"prefix"` +} + +type NodeSortRequest struct { + Sort []SortItem `json:"sort"` +} + +type NodeStatus struct { + Online interface{} `json:"online"` + Cpu float64 `json:"cpu"` + Mem float64 `json:"mem"` + Disk float64 `json:"disk"` + UpdatedAt int64 `json:"updated_at"` +} + +type OAthLoginRequest struct { + Method string `json:"method" validate:"required"` // google, facebook, apple, telegram, github etc. + Redirect string `json:"redirect"` +} + +type OAuthLoginGetTokenRequest struct { + Method string `json:"method" validate:"required"` // google, facebook, apple, telegram, github etc. + Callback interface{} `json:"callback" validate:"required"` +} + +type OAuthLoginResponse struct { + Redirect string `json:"redirect"` +} + +type OnlineUser struct { + SID int64 `json:"uid"` + IP string `json:"ip"` +} + +type OnlineUsersRequest struct { + ServerCommon + Users []OnlineUser `json:"users"` +} + +type Order struct { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + OrderNo string `json:"order_no"` + Type uint8 `json:"type"` + Quantity int64 `json:"quantity"` + Price int64 `json:"price"` + Amount int64 `json:"amount"` + GiftAmount int64 `json:"gift_amount"` + Discount int64 `json:"discount"` + Coupon string `json:"coupon"` + CouponDiscount int64 `json:"coupon_discount"` + Commission int64 `json:"commission,omitempty"` + Payment PaymentMethod `json:"payment"` + FeeAmount int64 `json:"fee_amount"` + TradeNo string `json:"trade_no"` + Status uint8 `json:"status"` + SubscribeId int64 `json:"subscribe_id"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type OrderDetail struct { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + OrderNo string `json:"order_no"` + Type uint8 `json:"type"` + Quantity int64 `json:"quantity"` + Price int64 `json:"price"` + Amount int64 `json:"amount"` + GiftAmount int64 `json:"gift_amount"` + Discount int64 `json:"discount"` + Coupon string `json:"coupon"` + CouponDiscount int64 `json:"coupon_discount"` + Commission int64 `json:"commission,omitempty"` + Payment PaymentMethod `json:"payment"` + Method string `json:"method"` + FeeAmount int64 `json:"fee_amount"` + TradeNo string `json:"trade_no"` + Status uint8 `json:"status"` + SubscribeId int64 `json:"subscribe_id"` + Subscribe Subscribe `json:"subscribe"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type OrdersStatistics struct { + Date string `json:"date,omitempty"` + AmountTotal int64 `json:"amount_total"` + NewOrderAmount int64 `json:"new_order_amount"` + RenewalOrderAmount int64 `json:"renewal_order_amount"` + List []OrdersStatistics `json:"list,omitempty"` +} + +type PaymentConfig struct { + Id int64 `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` + Platform string `json:"platform" validate:"required"` + Description string `json:"description"` + Icon string `json:"icon,omitempty"` + Domain string `json:"domain,omitempty"` + Config interface{} `json:"config" validate:"required"` + FeeMode uint `json:"fee_mode"` + FeePercent int64 `json:"fee_percent,omitempty"` + FeeAmount int64 `json:"fee_amount,omitempty"` + Enable *bool `json:"enable" validate:"required"` +} + +type PaymentMethod struct { + Id int64 `json:"id"` + Name string `json:"name"` + Platform string `json:"platform"` + Description string `json:"description"` + Icon string `json:"icon"` + FeeMode uint `json:"fee_mode"` + FeePercent int64 `json:"fee_percent"` + FeeAmount int64 `json:"fee_amount"` +} + +type PaymentMethodDetail struct { + Id int64 `json:"id"` + Name string `json:"name"` + Platform string `json:"platform"` + Description string `json:"description"` + Icon string `json:"icon"` + Domain string `json:"domain"` + Config interface{} `json:"config"` + FeeMode uint `json:"fee_mode"` + FeePercent int64 `json:"fee_percent"` + FeeAmount int64 `json:"fee_amount"` + Enable bool `json:"enable"` + NotifyURL string `json:"notify_url"` +} + +type PlatformInfo struct { + Platform string `json:"platform"` + PlatformUrl string `json:"platform_url"` + PlatformFieldDescription map[string]string `json:"platform_field_description"` +} + +type PlatformResponse struct { + List []PlatformInfo `json:"list"` +} + +type PortalPurchaseRequest struct { + AuthType string `json:"auth_type"` + Identifier string `json:"identifier"` + Password string `json:"password,omitempty"` + Payment int64 `json:"payment"` + SubscribeId int64 `json:"subscribe_id"` + Quantity int64 `json:"quantity"` + Coupon string `json:"coupon,omitempty"` + TurnstileToken string `json:"turnstile_token,omitempty"` +} + +type PortalPurchaseResponse struct { + OrderNo string `json:"order_no"` +} + +type PreOrderResponse struct { + Price int64 `json:"price"` + Amount int64 `json:"amount"` + Discount int64 `json:"discount"` + GiftAmount int64 `json:"gift_amount"` + Coupon string `json:"coupon"` + CouponDiscount int64 `json:"coupon_discount"` + FeeAmount int64 `json:"fee_amount"` +} + +type PrePurchaseOrderRequest struct { + Payment int64 `json:"payment,omitempty"` + SubscribeId int64 `json:"subscribe_id"` + Quantity int64 `json:"quantity"` + Coupon string `json:"coupon,omitempty"` +} + +type PrePurchaseOrderResponse struct { + Price int64 `json:"price"` + Amount int64 `json:"amount"` + Discount int64 `json:"discount"` + Coupon string `json:"coupon"` + CouponDiscount int64 `json:"coupon_discount"` + FeeAmount int64 `json:"fee_amount"` +} + +type PreRenewalOrderResponse struct { + OrderNo string `json:"orderNo"` +} + +type PreUnsubscribeRequest struct { + Id int64 `json:"id"` +} + +type PreUnsubscribeResponse struct { + DeductionAmount int64 `json:"deduction_amount"` +} + +type PrivacyPolicyConfig struct { + PrivacyPolicy string `json:"privacy_policy"` +} + +type PubilcRegisterConfig struct { + StopRegister bool `json:"stop_register"` + EnableIpRegisterLimit bool `json:"enable_ip_register_limit"` + IpRegisterLimit int64 `json:"ip_register_limit"` + IpRegisterLimitDuration int64 `json:"ip_register_limit_duration"` +} + +type PubilcVerifyCodeConfig struct { + VerifyCodeInterval int64 `json:"verify_code_interval"` +} + +type PurchaseOrderRequest struct { + SubscribeId int64 `json:"subscribe_id"` + Quantity int64 `json:"quantity"` + Payment int64 `json:"payment,omitempty"` + Coupon string `json:"coupon,omitempty"` +} + +type PurchaseOrderResponse struct { + OrderNo string `json:"order_no"` +} + +type QueryAnnouncementRequest struct { + Page int `form:"page"` + Size int `form:"size"` + Pinned *bool `form:"pinned"` + Popup *bool `form:"popup"` +} + +type QueryAnnouncementResponse struct { + Total int64 `json:"total"` + List []Announcement `json:"announcements"` +} + +type QueryDocumentDetailRequest struct { + Id int64 `form:"id" validate:"required"` +} + +type QueryDocumentListResponse struct { + Total int64 `json:"total"` + List []Document `json:"list"` +} + +type QueryOrderDetailRequest struct { + OrderNo string `form:"order_no" validate:"required"` +} + +type QueryOrderListRequest struct { + Page int `form:"page" validate:"required"` + Size int `form:"size" validate:"required"` +} + +type QueryOrderListResponse struct { + Total int64 `json:"total"` + List []OrderDetail `json:"list"` +} + +type QueryPurchaseOrderRequest struct { + AuthType string `form:"auth_type"` + Identifier string `form:"identifier"` + OrderNo string `form:"order_no"` +} + +type QueryPurchaseOrderResponse struct { + OrderNo string `json:"order_no"` + Subscribe Subscribe `json:"subscribe"` + Quantity int64 `json:"quantity"` + Price int64 `json:"price"` + Amount int64 `json:"amount"` + Discount int64 `json:"discount"` + Coupon string `json:"coupon"` + CouponDiscount int64 `json:"coupon_discount"` + FeeAmount int64 `json:"fee_amount"` + Payment PaymentMethod `json:"payment"` + Status uint8 `json:"status"` + CreatedAt int64 `json:"created_at"` + Token string `json:"token,omitempty"` +} + +type QuerySubscribeGroupListResponse struct { + List []SubscribeGroup `json:"list"` + Total int64 `json:"total"` +} + +type QuerySubscribeListResponse struct { + List []Subscribe `json:"list"` + Total int64 `json:"total"` +} + +type QueryUserAffiliateCountResponse struct { + Registers int64 `json:"registers"` + TotalCommission int64 `json:"total_commission"` +} + +type QueryUserAffiliateListRequest struct { + Page int `form:"page"` + Size int `form:"size"` +} + +type QueryUserAffiliateListResponse struct { + List []UserAffiliate `json:"list"` + Total int64 `json:"total"` +} + +type QueryUserBalanceLogListResponse struct { + List []UserBalanceLog `json:"list"` + Total int64 `json:"total"` +} + +type QueryUserCommissionLogListRequest struct { + Page int `form:"page"` + Size int `form:"size"` +} + +type QueryUserCommissionLogListResponse struct { + List []CommissionLog `json:"list"` + Total int64 `json:"total"` +} + +type QueryUserSubscribeListResponse struct { + List []UserSubscribe `json:"list"` + Total int64 `json:"total"` +} + +type QueryUserSubscribeResp struct { + Data []UserSubscribeData `json:"data"` +} + +type RechargeOrderRequest struct { + Amount int64 `json:"amount"` + Payment int64 `json:"payment"` +} + +type RechargeOrderResponse struct { + OrderNo string `json:"order_no"` +} + +type RegisterConfig struct { + StopRegister bool `json:"stop_register"` + EnableTrial bool `json:"enable_trial"` + TrialSubscribe int64 `json:"trial_subscribe"` + TrialTime int64 `json:"trial_time"` + TrialTimeUnit string `json:"trial_time_unit"` + EnableIpRegisterLimit bool `json:"enable_ip_register_limit"` + IpRegisterLimit int64 `json:"ip_register_limit"` + IpRegisterLimitDuration int64 `json:"ip_register_limit_duration"` +} + +type RenewalOrderRequest struct { + UserSubscribeID int64 `json:"user_subscribe_id"` + Quantity int64 `json:"quantity"` + Payment int64 `json:"payment"` + Coupon string `json:"coupon,omitempty"` +} + +type RenewalOrderResponse struct { + OrderNo string `json:"order_no"` +} + +type ResetPasswordRequest struct { + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + Code string `json:"code,optional"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + CfToken string `json:"cf_token,optional"` +} + +type ResetTrafficOrderRequest struct { + UserSubscribeID int64 `json:"user_subscribe_id"` + Payment int64 `json:"payment"` +} + +type ResetTrafficOrderResponse struct { + OrderNo string `json:"order_no"` +} + +type ResetUserSubscribeTokenRequest struct { + UserSubscribeId int64 `json:"user_subscribe_id"` +} + +type RevenueStatisticsResponse struct { + Today OrdersStatistics `json:"today"` + Monthly OrdersStatistics `json:"monthly"` + All OrdersStatistics `json:"all"` +} + +type SecurityConfig struct { + SNI string `json:"sni"` + AllowInsecure *bool `json:"allow_insecure"` + Fingerprint string `json:"fingerprint"` + RealityServerAddr string `json:"reality_server_addr"` + RealityServerPort int `json:"reality_server_port"` + RealityPrivateKey string `json:"reality_private_key"` + RealityPublicKey string `json:"reality_public_key"` + RealityShortId string `json:"reality_short_id"` +} + +type SendCodeRequest struct { + Email string `json:"email" validate:"required"` + Type uint8 `json:"type" validate:"required"` +} + +type SendCodeResponse struct { + Code string `json:"code,omitempty"` + Status bool `json:"status"` +} + +type SendSmsCodeRequest struct { + Type uint8 `json:"type" validate:"required"` + Telephone string `json:"telephone" validate:"required"` + TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` +} + +type Server struct { + Id int64 `json:"id"` + Tags []string `json:"tags"` + Country string `json:"country"` + City string `json:"city"` + Name string `json:"name"` + ServerAddr string `json:"server_addr"` + RelayMode string `json:"relay_mode"` + RelayNode []NodeRelay `json:"relay_node"` + SpeedLimit int `json:"speed_limit"` + TrafficRatio float32 `json:"traffic_ratio"` + GroupId int64 `json:"group_id"` + Protocol string `json:"protocol"` + Config interface{} `json:"config"` + Enable *bool `json:"enable"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + Status *NodeStatus `json:"status"` + Sort int64 `json:"sort"` +} + +type ServerBasic struct { + PushInterval int64 `json:"push_interval"` + PullInterval int64 `json:"pull_interval"` +} + +type ServerCommon struct { + Protocol string `form:"protocol"` + ServerId int64 `form:"server_id"` + SecretKey string `form:"secret_key"` +} + +type ServerGroup struct { + Id int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type ServerPushStatusRequest struct { + ServerCommon + Cpu float64 `json:"cpu"` + Mem float64 `json:"mem"` + Disk float64 `json:"disk"` + UpdatedAt int64 `json:"updated_at"` +} + +type ServerPushUserTrafficRequest struct { + ServerCommon + Traffic []UserTraffic `json:"traffic"` +} + +type ServerRuleGroup struct { + Id int64 `json:"id"` + Icon string `json:"icon"` + Name string `json:"name" validate:"required"` + Tags []string `json:"tags"` + Rules string `json:"rules"` + Enable bool `json:"enable"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type ServerTotalDataResponse struct { + OnlineUserIPs int64 `json:"online_user_ips"` + OnlineServers int64 `json:"online_servers"` + OfflineServers int64 `json:"offline_servers"` + TodayUpload int64 `json:"today_upload"` + TodayDownload int64 `json:"today_download"` + MonthlyUpload int64 `json:"monthly_upload"` + MonthlyDownload int64 `json:"monthly_download"` + UpdatedAt int64 `json:"updated_at"` + ServerTrafficRankingToday []ServerTrafficData `json:"server_traffic_ranking_today"` + ServerTrafficRankingYesterday []ServerTrafficData `json:"server_traffic_ranking_yesterday"` + UserTrafficRankingToday []UserTrafficData `json:"user_traffic_ranking_today"` + UserTrafficRankingYesterday []UserTrafficData `json:"user_traffic_ranking_yesterday"` +} + +type ServerTrafficData struct { + ServerId int64 `json:"server_id"` + Name string `json:"name"` + Upload int64 `json:"upload"` + Download int64 `json:"download"` +} + +type ServerUser struct { + Id int64 `json:"id"` + UUID string `json:"uuid"` + SpeedLimit int64 `json:"speed_limit"` + DeviceLimit int64 `json:"device_limit"` +} + +type SetNodeMultiplierRequest struct { + Periods []TimePeriod `json:"periods"` +} + +type Shadowsocks struct { + Method string `json:"method" validate:"required"` + Port int `json:"port" validate:"required"` + ServerKey string `json:"server_key"` +} + +type ShadowsocksProtocol struct { + Port int `json:"port"` + Method string `json:"method"` +} + +type SiteConfig struct { + Host string `json:"host"` + SiteName string `json:"site_name"` + SiteDesc string `json:"site_desc"` + SiteLogo string `json:"site_logo"` + Keywords string `json:"keywords"` + CustomHTML string `json:"custom_html"` + CustomData string `json:"custom_data"` +} + +type SiteCustomDataContacts struct { + Email string `json:"email"` + Telephone string `json:"telephone"` + Address string `json:"address"` +} + +type SortItem struct { + Id int64 `json:"id" validate:"required"` + Sort int64 `json:"sort" validate:"required"` +} + +type StripePayment struct { + Method string `json:"method"` + ClientSecret string `json:"client_secret"` + PublishableKey string `json:"publishable_key"` +} + +type Subscribe struct { + Id int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + UnitPrice int64 `json:"unit_price"` + UnitTime string `json:"unit_time"` + Discount []SubscribeDiscount `json:"discount"` + Replacement int64 `json:"replacement"` + Inventory int64 `json:"inventory"` + Traffic int64 `json:"traffic"` + SpeedLimit int64 `json:"speed_limit"` + DeviceLimit int64 `json:"device_limit"` + Quota int64 `json:"quota"` + GroupId int64 `json:"group_id"` + ServerGroup []int64 `json:"server_group"` + Server []int64 `json:"server"` + Show bool `json:"show"` + Sell bool `json:"sell"` + Sort int64 `json:"sort"` + DeductionRatio int64 `json:"deduction_ratio"` + AllowDeduction bool `json:"allow_deduction"` + ResetCycle int64 `json:"reset_cycle"` + RenewalReset bool `json:"renewal_reset"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type SubscribeConfig struct { + SingleModel bool `json:"single_model"` + SubscribePath string `json:"subscribe_path"` + SubscribeDomain string `json:"subscribe_domain"` + PanDomain bool `json:"pan_domain"` +} + +type SubscribeDiscount struct { + Quantity int64 `json:"quantity"` + Discount int64 `json:"discount"` +} + +type SubscribeGroup struct { + Id int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type SubscribeItem struct { + Subscribe + Sold int64 `json:"sold"` +} + +type SubscribeSortRequest struct { + Sort []SortItem `json:"sort"` +} + +type SubscribeType struct { + SubscribeTypes []string `json:"subscribe_types"` +} + +type TelegramConfig struct { + TelegramBotToken string `json:"telegram_bot_token"` + TelegramGroupUrl string `json:"telegram_group_url"` + TelegramNotify bool `json:"telegram_notify"` + TelegramWebHookDomain string `json:"telegram_web_hook_domain"` +} + +type TelephoneCheckUserRequest struct { + Telephone string `form:"telephone" validate:"required"` + TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` +} + +type TelephoneCheckUserResponse struct { + Exist bool `json:"exist"` +} + +type TelephoneLoginRequest struct { + Telephone string `json:"telephone" validate:"required"` + TelephoneCode string `json:"telephone_code"` + TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` + Password string `json:"password"` + IP string `header:"X-Original-Forwarded-For"` + CfToken string `json:"cf_token,optional"` +} + +type TelephoneRegisterRequest struct { + Telephone string `json:"telephone" validate:"required"` + TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` + Password string `json:"password" validate:"required"` + Invite string `json:"invite,optional"` + Code string `json:"code,optional"` + IP string `header:"X-Original-Forwarded-For"` + CfToken string `json:"cf_token,optional"` +} + +type TelephoneResetPasswordRequest struct { + Telephone string `json:"telephone" validate:"required"` + TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` + Password string `json:"password" validate:"required"` + Code string `json:"code,optional"` + IP string `header:"X-Original-Forwarded-For"` + CfToken string `json:"cf_token,optional"` +} + +type TestEmailSendRequest struct { + Email string `json:"email" validate:"required"` +} + +type TestSmsSendRequest struct { + AreaCode string `json:"area_code" validate:"required"` + Telephone string `json:"telephone" validate:"required"` +} + +type Ticket struct { + Id int64 `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + UserId int64 `json:"user_id"` + Follows []Follow `json:"follow,omitempty"` + Status uint8 `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type TicketWaitRelpyResponse struct { + Count int64 `json:"count"` +} + +type TimePeriod struct { + StartTime string `json:"start_time"` + EndTime string `json:"end_time"` + Multiplier float32 `json:"multiplier"` +} + +type TosConfig struct { + TosContent string `json:"tos_content"` +} + +type TrafficLog struct { + Id int64 `json:"id"` + ServerId int64 `json:"server_id"` + UserId int64 `json:"user_id"` + SubscribeId int64 `json:"subscribe_id"` + Download int64 `json:"download"` + Upload int64 `json:"upload"` + Timestamp int64 `json:"timestamp"` +} + +type TransportConfig struct { + Path string `json:"path"` + Host string `json:"host"` + ServiceName string `json:"service_name"` +} + +type Trojan struct { + Port int `json:"port" validate:"required"` + Transport string `json:"transport" validate:"required"` + TransportConfig TransportConfig `json:"transport_config"` + Security string `json:"security" validate:"required"` + SecurityConfig SecurityConfig `json:"security_config"` +} + +type TrojanProtocol struct { + Host string `json:"host"` + Port int `json:"port"` + EnableTLS *bool `json:"enable_tls"` + TLSConfig string `json:"tls_config"` + Network string `json:"network"` + Transport string `json:"transport"` +} + +type Tuic struct { + Port int `json:"port" validate:"required"` + SecurityConfig SecurityConfig `json:"security_config"` +} + +type UnbindOAuthRequest struct { + Method string `json:"method"` +} + +type UnsubscribeRequest struct { + Id int64 `json:"id"` +} + +type UpdateAdsRequest struct { + Id int64 `json:"id"` + Title string `json:"title"` + Type string `json:"type"` + Content string `json:"content"` + Description string `json:"description"` + TargetURL string `json:"target_url"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + Status int `json:"status"` +} + +type UpdateAnnouncementEnableRequest struct { + Id int64 `json:"id" validate:"required"` + Enable *bool `json:"enable" validate:"required"` +} + +type UpdateAnnouncementRequest struct { + Id int64 `json:"id" validate:"required"` + Title string `json:"title"` + Content string `json:"content"` + Show *bool `json:"show"` + Pinned *bool `json:"pinned"` + Popup *bool `json:"popup"` +} + +type UpdateApplicationRequest struct { + Id int64 `json:"id" validate:"required"` + Icon string `json:"icon"` + Name string `json:"name"` + Description string `json:"description"` + SubscribeType string `json:"subscribe_type"` + Platform ApplicationPlatform `json:"platform"` +} + +type UpdateApplicationVersionRequest struct { + Id int64 `json:"id" validate:"required"` + Url string `json:"url"` + Version string `json:"version" validate:"required"` + Description string `json:"description"` + Platform string `json:"platform" validate:"required,oneof=windows mac linux android ios harmony"` + IsDefault bool `json:"is_default"` + ApplicationId int64 `json:"application_id" validate:"required"` +} + +type UpdateAuthMethodConfigRequest struct { + Id int64 `json:"id"` + Method string `json:"method"` + Config interface{} `json:"config"` + Enabled *bool `json:"enabled"` +} + +type UpdateBindEmailRequest struct { + Email string `json:"email" validate:"required"` +} + +type UpdateBindMobileRequest struct { + AreaCode string `json:"area_code" validate:"required"` + Mobile string `json:"mobile" validate:"required"` + Code string `json:"code" validate:"required"` +} + +type UpdateCouponRequest struct { + Id int64 `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` + Code string `json:"code,omitempty"` + Count int64 `json:"count,omitempty"` + Type uint8 `json:"type" validate:"required"` + Discount int64 `json:"discount" validate:"required"` + StartTime int64 `json:"start_time" validate:"required"` + ExpireTime int64 `json:"expire_time" validate:"required"` + UserLimit int64 `json:"user_limit,omitempty"` + Subscribe []int64 `json:"subscribe,omitempty"` + UsedCount int64 `json:"used_count,omitempty"` + Enable *bool `json:"enable,omitempty"` +} + +type UpdateDocumentRequest struct { + Id int64 `json:"id" validate:"required"` + Title string `json:"title" validate:"required"` + Content string `json:"content" validate:"required"` + Tags []string `json:"tags,omitempty" ` + Show *bool `json:"show"` +} + +type UpdateNodeGroupRequest struct { + Id int64 `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` + Description string `json:"description"` +} + +type UpdateNodeRequest struct { + Id int64 `json:"id" validate:"required"` + Tags []string `json:"tags"` + Country string `json:"country"` + City string `json:"city"` + Name string `json:"name" validate:"required"` + ServerAddr string `json:"server_addr" validate:"required"` + RelayMode string `json:"relay_mode"` + RelayNode []NodeRelay `json:"relay_node"` + SpeedLimit int `json:"speed_limit"` + TrafficRatio float32 `json:"traffic_ratio"` + GroupId int64 `json:"group_id"` + Protocol string `json:"protocol" validate:"required"` + Config interface{} `json:"config" validate:"required"` + Enable *bool `json:"enable"` + Sort int64 `json:"sort"` +} + +type UpdateOrderStatusRequest struct { + Id int64 `json:"id" validate:"required"` + Status uint8 `json:"status" validate:"required"` + PaymentId int64 `json:"payment_id,omitempty"` + TradeNo string `json:"trade_no,omitempty"` +} + +type UpdatePasswordRequeset struct { + Password string `json:"password"` + NewPassword string `json:"new_password"` +} + +type UpdatePaymentMethodRequest struct { + Id int64 `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` + Platform string `json:"platform" validate:"required"` + Description string `json:"description"` + Icon string `json:"icon,omitempty"` + Domain string `json:"domain,omitempty"` + Config interface{} `json:"config" validate:"required"` + FeeMode uint `json:"fee_mode"` + FeePercent int64 `json:"fee_percent,omitempty"` + FeeAmount int64 `json:"fee_amount,omitempty"` + Enable *bool `json:"enable" validate:"required"` +} + +type UpdateRuleGroupRequest struct { + Id int64 `json:"id" validate:"required"` + Icon string `json:"icon"` + Name string `json:"name" validate:"required"` + Tags []string `json:"tags"` + Rules string `json:"rules"` + Enable bool `json:"enable"` +} + +type UpdateSubscribeGroupRequest struct { + Id int64 `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` + Description string `json:"description"` +} + +type UpdateSubscribeRequest struct { + Id int64 `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` + Description string `json:"description"` + UnitPrice int64 `json:"unit_price"` + UnitTime string `json:"unit_time"` + Discount []SubscribeDiscount `json:"discount"` + Replacement int64 `json:"replacement"` + Inventory int64 `json:"inventory"` + Traffic int64 `json:"traffic"` + SpeedLimit int64 `json:"speed_limit"` + DeviceLimit int64 `json:"device_limit"` + Quota int64 `json:"quota"` + GroupId int64 `json:"group_id"` + ServerGroup []int64 `json:"server_group"` + Server []int64 `json:"server"` + Show *bool `json:"show"` + Sell *bool `json:"sell"` + Sort int64 `json:"sort"` + DeductionRatio int64 `json:"deduction_ratio"` + AllowDeduction *bool `json:"allow_deduction"` + ResetCycle int64 `json:"reset_cycle"` + RenewalReset *bool `json:"renewal_reset"` +} + +type UpdateTicketStatusRequest struct { + Id int64 `json:"id" validate:"required"` + Status *uint8 `json:"status" validate:"required"` +} + +type UpdateUserAuthMethodRequest struct { + UserId int64 `json:"user_id"` + AuthType string `json:"auth_type"` + AuthIdentifier string `json:"auth_identifier"` +} + +type UpdateUserBasiceInfoRequest struct { + UserId int64 `json:"user_id" validate:"required"` + Password string `json:"password"` + Avatar string `json:"avatar"` + Balance int64 `json:"balance"` + Commission int64 `json:"commission"` + GiftAmount int64 `json:"gift_amount"` + Telegram int64 `json:"telegram"` + ReferCode string `json:"refer_code"` + RefererId int64 `json:"referer_id"` + Enable bool `json:"enable"` + IsAdmin bool `json:"is_admin"` +} + +type UpdateUserNotifyRequest struct { + EnableBalanceNotify *bool `json:"enable_balance_notify"` + EnableLoginNotify *bool `json:"enable_login_notify"` + EnableSubscribeNotify *bool `json:"enable_subscribe_notify"` + EnableTradeNotify *bool `json:"enable_trade_notify"` +} + +type UpdateUserNotifySettingRequest struct { + UserId int64 `json:"user_id" validate:"required"` + EnableBalanceNotify bool `json:"enable_balance_notify"` + EnableLoginNotify bool `json:"enable_login_notify"` + EnableSubscribeNotify bool `json:"enable_subscribe_notify"` + EnableTradeNotify bool `json:"enable_trade_notify"` +} + +type UpdateUserPasswordRequest struct { + Password string `json:"password" validate:"required"` +} + +type UpdateUserSubscribeRequest struct { + UserSubscribeId int64 `json:"user_subscribe_id"` + SubscribeId int64 `json:"subscribe_id"` + Traffic int64 `json:"traffic"` + ExpiredAt int64 `json:"expired_at"` + Upload int64 `json:"upload"` + Download int64 `json:"download"` +} + +type UpdateUserTicketStatusRequest struct { + Id int64 `json:"id" validate:"required"` + Status *uint8 `json:"status" validate:"required"` +} + +type User struct { + Id int64 `json:"id"` + Avatar string `json:"avatar"` + Balance int64 `json:"balance"` + Commission int64 `json:"commission"` + GiftAmount int64 `json:"gift_amount"` + Telegram int64 `json:"telegram"` + ReferCode string `json:"refer_code"` + RefererId int64 `json:"referer_id"` + Enable bool `json:"enable"` + IsAdmin bool `json:"is_admin,omitempty"` + EnableBalanceNotify bool `json:"enable_balance_notify"` + EnableLoginNotify bool `json:"enable_login_notify"` + EnableSubscribeNotify bool `json:"enable_subscribe_notify"` + EnableTradeNotify bool `json:"enable_trade_notify"` + AuthMethods []UserAuthMethod `json:"auth_methods"` + UserDevices []UserDevice `json:"user_devices"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + DeletedAt int64 `json:"deleted_at,omitempty"` + IsDel bool `json:"is_del,omitempty"` +} + +type UserAffiliate struct { + Avatar string `json:"avatar"` + Identifier string `json:"identifier"` + RegisteredAt int64 `json:"registered_at"` + Enable bool `json:"enable"` +} + +type UserAuthMethod struct { + AuthType string `json:"auth_type"` + AuthIdentifier string `json:"auth_identifier"` + Verified bool `json:"verified"` +} + +type UserBalanceLog struct { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + Amount int64 `json:"amount"` + Type uint8 `json:"type"` + OrderId int64 `json:"order_id"` + Balance int64 `json:"balance"` + CreatedAt int64 `json:"created_at"` +} + +type UserDevice struct { + Id int64 `json:"id"` + Ip string `json:"ip"` + Identifier string `json:"identifier"` + UserAgent string `json:"user_agent"` + Online bool `json:"online"` + Enabled bool `json:"enabled"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type UserInfoResponse struct { + Id int64 `json:"id"` + Balance int64 `json:"balance"` + Email string `json:"email"` + RefererId int64 `json:"referer_id"` + ReferCode string `json:"refer_code"` + Avatar string `json:"avatar"` + AreaCode string `json:"area_code"` + Telephone string `json:"telephone"` + Devices []UserDevice `json:"devices"` + AuthMethods []UserAuthMethod `json:"auth_methods"` +} + +type UserLoginLog struct { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + LoginIP string `json:"login_ip"` + UserAgent string `json:"user_agent"` + Success bool `json:"success"` + CreatedAt int64 `json:"created_at"` +} + +type UserLoginRequest struct { + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + CfToken string `json:"cf_token,optional"` +} + +type UserRegisterRequest struct { + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + Invite string `json:"invite,optional"` + Code string `json:"code,optional"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + CfToken string `json:"cf_token,optional"` +} + +type UserStatistics struct { + Date string `json:"date,omitempty"` + Register int64 `json:"register"` + NewOrderUsers int64 `json:"new_order_users"` + RenewalOrderUsers int64 `json:"renewal_order_users"` + List []UserStatistics `json:"list,omitempty"` +} + +type UserStatisticsResponse struct { + Today UserStatistics `json:"today"` + Monthly UserStatistics `json:"monthly"` + All UserStatistics `json:"all"` +} + +type UserSubscribe struct { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + OrderId int64 `json:"order_id"` + SubscribeId int64 `json:"subscribe_id"` + Subscribe Subscribe `json:"subscribe"` + StartTime int64 `json:"start_time"` + ExpireTime int64 `json:"expire_time"` + FinishedAt int64 `json:"finished_at"` + ResetTime int64 `json:"reset_time"` + Traffic int64 `json:"traffic"` + Download int64 `json:"download"` + Upload int64 `json:"upload"` + Token string `json:"token"` + Status uint8 `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type UserSubscribeData struct { + SubscribeId int64 `json:"subscribe_id"` + UserSubscribeId int64 `json:"user_subscribe_id"` +} + +type UserSubscribeDetail struct { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + User User `json:"user"` + OrderId int64 `json:"order_id"` + SubscribeId int64 `json:"subscribe_id"` + Subscribe Subscribe `json:"subscribe"` + StartTime int64 `json:"start_time"` + ExpireTime int64 `json:"expire_time"` + ResetTime int64 `json:"reset_time"` + Traffic int64 `json:"traffic"` + Download int64 `json:"download"` + Upload int64 `json:"upload"` + Token string `json:"token"` + Status uint8 `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type UserSubscribeLog struct { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + UserSubscribeId int64 `json:"user_subscribe_id"` + Token string `json:"token"` + IP string `json:"ip"` + UserAgent string `json:"user_agent"` + CreatedAt int64 `json:"created_at"` +} + +type UserSubscribeResetPeriodRequest struct { + UserSubscribeId int64 `json:"user_subscribe_id"` +} + +type UserSubscribeResetPeriodResponse struct { + Status bool `json:"status"` +} + +type UserTraffic struct { + SID int64 `json:"uid"` + Upload int64 `json:"upload"` + Download int64 `json:"download"` +} + +type UserTrafficData struct { + SID int64 `json:"sid"` + Upload int64 `json:"upload"` + Download int64 `json:"download"` +} + +type VeifyConfig struct { + TurnstileSiteKey string `json:"turnstile_site_key"` + EnableLoginVerify bool `json:"enable_login_verify"` + EnableRegisterVerify bool `json:"enable_register_verify"` + EnableResetPasswordVerify bool `json:"enable_reset_password_verify"` +} + +type VerifyCodeConfig struct { + VerifyCodeExpireTime int64 `json:"verify_code_expire_time"` + VerifyCodeLimit int64 `json:"verify_code_limit"` + VerifyCodeInterval int64 `json:"verify_code_interval"` +} + +type VerifyConfig struct { + TurnstileSiteKey string `json:"turnstile_site_key"` + TurnstileSecret string `json:"turnstile_secret"` + EnableLoginVerify bool `json:"enable_login_verify"` + EnableRegisterVerify bool `json:"enable_register_verify"` + EnableResetPasswordVerify bool `json:"enable_reset_password_verify"` +} + +type VerifyEmailRequest struct { + Email string `json:"email" validate:"required"` + Code string `json:"code" validate:"required"` +} + +type Vless struct { + Port int `json:"port" validate:"required"` + Flow string `json:"flow" validate:"required"` + Transport string `json:"transport" validate:"required"` + TransportConfig TransportConfig `json:"transport_config"` + Security string `json:"security" validate:"required"` + SecurityConfig SecurityConfig `json:"security_config"` +} + +type VlessProtocol struct { + Host string `json:"host"` + Port int `json:"port"` + Network string `json:"network"` + Transport string `json:"transport"` + Security string `json:"security"` + SecurityConfig string `json:"security_config"` + XTLS string `json:"xtls"` +} + +type Vmess struct { + Port int `json:"port" validate:"required"` + Transport string `json:"transport" validate:"required"` + TransportConfig TransportConfig `json:"transport_config"` + Security string `json:"security" validate:"required"` + SecurityConfig SecurityConfig `json:"security_config"` +} + +type VmessProtocol struct { + Host string `json:"host"` + Port int `json:"port"` + EnableTLS *bool `json:"enable_tls"` + TLSConfig string `json:"tls_config"` + Network string `json:"network"` + Transport string `json:"transport"` +} + +type WeeklyStat struct { + Day int `json:"day"` + DayName string `json:"day_name"` + Hours float64 `json:"hours"` +} diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000..6290468 --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,18 @@ +pre-commit: + parallel: true + commands: + go-fmt: + run: go fmt ./... + go-imports: + run: goimports -w . + go-lint: + run: golangci-lint run + go-vet: + run: go vet ./... + go-test: + run: go test -v ./... + +commit-msg: + commands: + commitlint: + run: npx --no -- commitlint --edit $1 diff --git a/pkg/adapter/adapter.go b/pkg/adapter/adapter.go new file mode 100644 index 0000000..aaec788 --- /dev/null +++ b/pkg/adapter/adapter.go @@ -0,0 +1,71 @@ +package adapter + +import ( + "github.com/perfect-panel/ppanel-server/internal/model/server" + "github.com/perfect-panel/ppanel-server/pkg/adapter/clash" + "github.com/perfect-panel/ppanel-server/pkg/adapter/general" + "github.com/perfect-panel/ppanel-server/pkg/adapter/loon" + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" + "github.com/perfect-panel/ppanel-server/pkg/adapter/quantumultx" + "github.com/perfect-panel/ppanel-server/pkg/adapter/shadowrocket" + "github.com/perfect-panel/ppanel-server/pkg/adapter/singbox" + "github.com/perfect-panel/ppanel-server/pkg/adapter/surfboard" +) + +type Adapter struct { + proxy.Adapter +} + +func NewAdapter(nodes []*server.Server, rules []*server.RuleGroup) *Adapter { + // 转换服务器列表 + proxies := adapterProxies(nodes) + // 生成代理组 + proxyGroup, region := generateProxyGroup(proxies) + // 转换规则组 + g, r := adapterRules(rules) + // 加入兜底节点 + for i, group := range g { + if len(group.Proxies) == 0 { + g[i].Proxies = append([]string{"DIRECT"}, region...) + } + } + // 合并代理组 + proxyGroup = append(proxyGroup, g...) + return &Adapter{ + Adapter: proxy.Adapter{ + Proxies: proxies, + Group: proxyGroup, + Rules: r, + Region: region, + }, + } +} + +func (m *Adapter) BuildClash(uuid string) ([]byte, error) { + client := clash.NewClash(m.Adapter) + return client.Build(uuid) +} + +func (m *Adapter) BuildGeneral(uuid string) []byte { + return general.GenerateBase64General(m.Proxies, uuid) +} + +func (m *Adapter) BuildLoon(uuid string) []byte { + return loon.BuildLoon(m.Proxies, uuid) +} + +func (m *Adapter) BuildQuantumultX(uuid string) string { + return quantumultx.BuildQuantumultX(m.Proxies, uuid) +} + +func (m *Adapter) BuildSingbox(uuid string) ([]byte, error) { + return singbox.BuildSingbox(m.Adapter, uuid) +} + +func (m *Adapter) BuildShadowrocket(uuid string, userInfo shadowrocket.UserInfo) []byte { + return shadowrocket.BuildShadowrocket(m.Proxies, uuid, userInfo) +} + +func (m *Adapter) BuildSurfboard(siteName string, user surfboard.UserInfo) []byte { + return surfboard.BuildSurfboard(m.Adapter, siteName, user) +} diff --git a/pkg/adapter/adapter_test.go b/pkg/adapter/adapter_test.go new file mode 100644 index 0000000..25c5a64 --- /dev/null +++ b/pkg/adapter/adapter_test.go @@ -0,0 +1,138 @@ +package adapter + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/perfect-panel/ppanel-server/internal/model/server" + "github.com/perfect-panel/ppanel-server/pkg/adapter/surfboard" +) + +func createTestServer() []*server.Server { + c := server.Shadowsocks{ + Method: "aes-256-gcm", + Port: 10301, + ServerKey: "", + } + data, _ := json.Marshal(c) + + relays := creatRelayNode() + relay, _ := json.Marshal(relays) + enable := true + // 创建一个测试用的服务器列表 + return []*server.Server{ + { + Id: 1, + Name: "Test Server 1", + Tags: "", + Country: "CN", + City: "", + Latitude: "", + Longitude: "", + ServerAddr: "test1.example.com", + RelayMode: "random", + RelayNode: string(relay), + SpeedLimit: 0, + TrafficRatio: 0, + GroupId: 0, + Protocol: "shadowsocks", + Config: string(data), + Enable: &enable, + Sort: 0, + }, + } +} +func creatRelayNode() []*server.NodeRelay { + var nodes []*server.NodeRelay + for i := 0; i < 10; i++ { + port := 10301 + i + c := server.NodeRelay{ + Host: fmt.Sprintf("192.168.1.%d", i), + Port: port, + Prefix: fmt.Sprintf("relay-%d", i), + } + nodes = append(nodes, &c) + } + return nodes +} + +func TestNewAdapter(t *testing.T) { + nodes := createTestServer() + + rules := []*server.RuleGroup{ + { + Name: "Test Rule Group 1", + Tags: "", + Rules: "DOMAIN-SUFFIX,example.com,Test Rule Group 1", + }, + } + + adapter := NewAdapter(nodes, rules) + bytes, err := adapter.BuildClash("some-uuid") + if err != nil { + t.Errorf("Failed to build adapter: %v", err) + return + } + t.Logf("Adapter built successfully: %s", string(bytes)) +} + +func TestAdapter_BuildSingbox(t *testing.T) { + nodes := createTestServer() + + rules := []*server.RuleGroup{ + { + Name: "Test Rule Group 1", + Tags: "", + Rules: "DOMAIN-SUFFIX,example.com,Test Rule Group 1", + }, + } + + adapter := NewAdapter(nodes, rules) + bytes, err := adapter.BuildSingbox("some-uuid") + if err != nil { + t.Errorf("Failed to build adapter: %v", err) + return + } + var pretty map[string]interface{} + _ = json.Unmarshal(bytes, &pretty) + + if pretty == nil { + t.Errorf("Failed to parse Singbox config") + return + } + + prettyStr, err := json.MarshalIndent(pretty, "", " ") + if err != nil { + t.Errorf("Failed to format Singbox config: %v", err) + return + } + t.Logf("Adapter built successfully: \n %s", string(prettyStr)) +} + +func TestAdapter_BuildSurfboard(t *testing.T) { + nodes := createTestServer() + rules := []*server.RuleGroup{ + { + Name: "Test Rule Group 1", + Tags: "", + Rules: "DOMAIN-SUFFIX,example.com,Test Rule Group 1", + }, + } + adapter := NewAdapter(nodes, rules) + user := surfboard.UserInfo{ + UUID: "some-uuid", + Upload: 200, + Download: 13012, + TotalTraffic: 1024000, + ExpiredDate: time.Now().Add(24 * time.Hour), + SubscribeURL: "", + } + bytes := adapter.BuildSurfboard("test-site", user) + if bytes == nil { + t.Errorf("Failed to build adapter") + return + } + t.Logf("Adapter built successfully: %s", string(bytes)) +} diff --git a/pkg/adapter/clash/clash.go b/pkg/adapter/clash/clash.go new file mode 100644 index 0000000..52bcf61 --- /dev/null +++ b/pkg/adapter/clash/clash.go @@ -0,0 +1,68 @@ +package clash + +import ( + "fmt" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "gopkg.in/yaml.v3" +) + +type Clash struct { + proxy.Adapter +} + +func NewClash(adapter proxy.Adapter) *Clash { + return &Clash{ + Adapter: adapter, + } +} + +func (c *Clash) Build(uuid string) ([]byte, error) { + var proxies []Proxy + for _, v := range c.Proxies { + p, err := c.parseProxy(v, uuid) + if err != nil { + logger.Errorf("Failed to parse proxy for %s: %s", v.Name, err.Error()) + continue + } + proxies = append(proxies, *p) + } + var rawConfig RawConfig + if err := yaml.Unmarshal([]byte(DefaultTemplate), &rawConfig); err != nil { + return nil, fmt.Errorf("failed to unmarshal template: %w", err) + } + rawConfig.Proxies = proxies + // generate proxy groups + var groups []ProxyGroup + for _, group := range c.Group { + groups = append(groups, ProxyGroup{ + Name: group.Name, + Type: string(group.Type), + Proxies: group.Proxies, + Url: group.URL, + Interval: group.Interval, + }) + } + rawConfig.ProxyGroups = groups + rawConfig.Rules = append(c.Rules, "# 最终规则", "MATCH,手动选择") + return yaml.Marshal(&rawConfig) +} + +func (c *Clash) parseProxy(p proxy.Proxy, uuid string) (*Proxy, error) { + parseFuncs := map[string]func(proxy.Proxy, string) (*Proxy, error){ + "shadowsocks": parseShadowsocks, + "trojan": parseTrojan, + "vless": parseVless, + "vmess": parseVmess, + "hysteria2": parseHysteria2, + "tuic": parseTuic, + } + + if parseFunc, exists := parseFuncs[p.Protocol]; exists { + return parseFunc(p, uuid) + } + + logger.Errorw("Unknown protocol", logger.Field("protocol", p.Protocol), logger.Field("server", p.Name)) + return nil, fmt.Errorf("unknown protocol: %s", p.Protocol) +} diff --git a/pkg/adapter/clash/clash_test.go b/pkg/adapter/clash/clash_test.go new file mode 100644 index 0000000..1ab8156 --- /dev/null +++ b/pkg/adapter/clash/clash_test.go @@ -0,0 +1,41 @@ +package clash + +import ( + "testing" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" + "github.com/stretchr/testify/assert" +) + +func TestClash_Build(t *testing.T) { + adapter := proxy.Adapter{ + Proxies: []proxy.Proxy{ + { + Name: "test-proxy", + Protocol: "shadowsocks", + Server: "1.2.3.4", + Port: 8388, + Option: proxy.Shadowsocks{ + Method: "aes-256-gcm", + }, + }, + }, + Group: []proxy.Group{ + { + Name: "test-group", + Type: "select", + Proxies: []string{"test-proxy"}, + }, + }, + Rules: []string{ + "DOMAIN-SUFFIX,example.com,DIRECT", + "GEOIP,CN,DIRECT", + "MATCH,DIRECT", + }, + } + clash := NewClash(adapter) + result, err := clash.Build("test-uuid") + assert.NoError(t, err) + assert.NotNil(t, result) + +} diff --git a/pkg/adapter/clash/default.go b/pkg/adapter/clash/default.go new file mode 100644 index 0000000..4766053 --- /dev/null +++ b/pkg/adapter/clash/default.go @@ -0,0 +1,35 @@ +package clash + +const DefaultTemplate = ` +mixed-port: 7890 +allow-lan: true +bind-address: "*" +mode: rule +log-level: info +external-controller: 127.0.0.1:9090 +global-client-fingerprint: chrome +unified-delay: true +geox-url: + mmdb: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.metadb" +dns: + enable: true + ipv6: true + enhanced-mode: fake-ip + fake-ip-range: 198.18.0.1/16 + use-hosts: true + default-nameserver: + - 120.53.53.53 + - 1.12.12.12 + nameserver: + - https://120.53.53.53/dns-query#skip-cert-verify=true + - tls://1.12.12.12#skip-cert-verify=true + proxy-server-nameserver: + - https://120.53.53.53/dns-query#skip-cert-verify=true + - tls://1.12.12.12#skip-cert-verify=true + +proxies: + +proxy-groups: + +rules: +` diff --git a/pkg/adapter/clash/model.go b/pkg/adapter/clash/model.go new file mode 100644 index 0000000..9f9925d --- /dev/null +++ b/pkg/adapter/clash/model.go @@ -0,0 +1,131 @@ +package clash + +type RawConfig struct { + Port int `yaml:"port" json:"port"` + SocksPort int `yaml:"socks-port" json:"socks-port"` + RedirPort int `yaml:"redir-port" json:"redir-port"` + TProxyPort int `yaml:"tproxy-port" json:"tproxy-port"` + MixedPort int `yaml:"mixed-port" json:"mixed-port"` + AllowLan bool `yaml:"allow-lan" json:"allow-lan"` + Mode string `yaml:"mode" json:"mode"` + LogLevel string `yaml:"log-level" json:"log-level"` + ExternalController string `yaml:"external-controller" json:"external-controller"` + Secret string `yaml:"secret" json:"secret"` + Proxies []Proxy `yaml:"proxies" json:"proxies"` + ProxyGroups []ProxyGroup `yaml:"proxy-groups" json:"proxy-groups"` + Rules []string `yaml:"rules" json:"rule"` +} + +type Proxy struct { + // 基础数据 + Name string `yaml:"name"` + Type string `yaml:"type"` + Server string `yaml:"server"` + Port int `yaml:"port,omitempty"` + // Shadowsocks + Password string `yaml:"password,omitempty"` + Cipher string `yaml:"cipher,omitempty"` + UDP bool `yaml:"udp,omitempty"` + Plugin string `yaml:"plugin,omitempty"` + PluginOpts map[string]any `yaml:"plugin-opts,omitempty"` + UDPOverTCP bool `yaml:"udp-over-tcp,omitempty"` + UDPOverTCPVersion int `yaml:"udp-over-tcp-version,omitempty"` + ClientFingerprint string `yaml:"client-fingerprint,omitempty"` + // Vmess + UUID string `yaml:"uuid,omitempty"` + AlterID *int `yaml:"alterId,omitempty"` + Network string `yaml:"network,omitempty"` + TLS bool `yaml:"tls,omitempty"` + ALPN []string `yaml:"alpn,omitempty"` + SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"` + Fingerprint string `yaml:"fingerprint,omitempty"` + ServerName string `yaml:"servername,omitempty"` + RealityOpts RealityOptions `yaml:"reality-opts,omitempty"` + HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"` + HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"` + GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"` + WSOpts WSOptions `yaml:"ws-opts,omitempty"` + PacketAddr bool `yaml:"packet-addr,omitempty"` + XUDP bool `yaml:"xudp,omitempty"` + PacketEncoding string `yaml:"packet-encoding,omitempty"` + GlobalPadding bool `yaml:"global-padding,omitempty"` + AuthenticatedLength bool `yaml:"authenticated-length,omitempty"` + // Vless + Flow string `yaml:"flow,omitempty"` + WSPath string `yaml:"ws-path,omitempty"` + WSHeaders map[string]string `yaml:"ws-headers,omitempty"` + // Trojan + SNI string `yaml:"sni,omitempty"` + SSOpts TrojanSSOption `yaml:"ss-opts,omitempty"` + // Hysteria2 + Ports string `yaml:"ports,omitempty"` + HopInterval int `yaml:"hop-interval,omitempty"` + Up string `yaml:"up,omitempty"` + Down string `yaml:"down,omitempty"` + Obfs string `yaml:"obfs,omitempty"` + ObfsPassword string `yaml:"obfs-password,omitempty"` + CustomCA string `yaml:"ca,omitempty"` + CustomCAString string `yaml:"ca-str,omitempty"` + CWND int `yaml:"cwnd,omitempty"` + UdpMTU int `yaml:"udp-mtu,omitempty"` + // Tuic + Token string `yaml:"token,omitempty"` + Ip string `yaml:"ip,omitempty"` + HeartbeatInterval int `yaml:"heartbeat-interval,omitempty"` + ReduceRtt bool `yaml:"reduce-rtt,omitempty"` + RequestTimeout int `yaml:"request-timeout,omitempty"` + UdpRelayMode string `yaml:"udp-relay-mode,omitempty"` + CongestionController string `yaml:"congestion-controller,omitempty"` + DisableSni bool `yaml:"disable-sni,omitempty"` + MaxUdpRelayPacketSize int `yaml:"max-udp-relay-packet-size,omitempty"` + FastOpen bool `yaml:"fast-open,omitempty"` + MaxOpenStreams int `yaml:"max-open-streams,omitempty"` + ReceiveWindowConn int `yaml:"recv-window-conn,omitempty"` + ReceiveWindow int `yaml:"recv-window,omitempty"` + DisableMTUDiscovery bool `yaml:"disable-mtu-discovery,omitempty"` + MaxDatagramFrameSize int `yaml:"max-datagram-frame-size,omitempty"` + UDPOverStream bool `yaml:"udp-over-stream,omitempty"` + UDPOverStreamVersion int `yaml:"udp-over-stream-version,omitempty"` +} +type ProxyGroup struct { + Name string `yaml:"name"` + Type string `yaml:"type"` + Proxies []string `yaml:"proxies"` + Url string `yaml:"url,omitempty"` + Interval int `yaml:"interval,omitempty"` +} + +type TrojanSSOption struct { + Enabled bool `yaml:"enabled,omitempty"` + Method string `yaml:"method,omitempty"` + Password string `yaml:"password,omitempty"` +} + +type RealityOptions struct { + PublicKey string `yaml:"public-key"` + ShortID string `yaml:"short-id"` +} + +type HTTPOptions struct { + Method string `yaml:"method,omitempty"` + Path []string `yaml:"path,omitempty"` + Headers map[string][]string `yaml:"headers,omitempty"` +} + +type HTTP2Options struct { + Host []string `yaml:"host,omitempty"` + Path string `yaml:"path,omitempty"` +} + +type GrpcOptions struct { + GrpcServiceName string `yaml:"grpc-service-name,omitempty"` +} + +type WSOptions struct { + Path string `yaml:"path,omitempty"` + Headers map[string]string `yaml:"headers,omitempty"` + MaxEarlyData int `yaml:"max-early-data,omitempty"` + EarlyDataHeaderName string `yaml:"early-data-header-name,omitempty"` + V2rayHttpUpgrade bool `yaml:"v2ray-http-upgrade,omitempty"` + V2rayHttpUpgradeFastOpen bool `yaml:"v2ray-http-upgrade-fast-open,omitempty"` +} diff --git a/pkg/adapter/clash/parse.go b/pkg/adapter/clash/parse.go new file mode 100644 index 0000000..a52c331 --- /dev/null +++ b/pkg/adapter/clash/parse.go @@ -0,0 +1,165 @@ +package clash + +import ( + "fmt" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func parseShadowsocks(s proxy.Proxy, uuid string) (*Proxy, error) { + config, ok := s.Option.(proxy.Shadowsocks) + if !ok { + return nil, fmt.Errorf("invalid type for Shadowsocks") + } + p := &Proxy{ + Name: s.Name, + Type: "ss", + Server: s.Server, + Port: s.Port, + Cipher: config.Method, + Password: uuid, + UDP: true, + } + + return p, nil +} + +func parseTrojan(data proxy.Proxy, password string) (*Proxy, error) { + trojan, ok := data.Option.(proxy.Trojan) + if !ok { + return nil, fmt.Errorf("invalid type for Trojan") + } + p := &Proxy{ + Name: data.Name, + Type: "trojan", + Server: data.Server, + Port: data.Port, + Password: password, + SNI: trojan.SecurityConfig.SNI, + SkipCertVerify: trojan.SecurityConfig.AllowInsecure, + } + setTransportOptions(p, trojan.Transport, trojan.TransportConfig) + return p, nil +} + +func parseVless(data proxy.Proxy, uuid string) (*Proxy, error) { + vless, ok := data.Option.(proxy.Vless) + if !ok { + return nil, fmt.Errorf("invalid type for Vless") + } + p := &Proxy{ + Name: data.Name, + Type: "vless", + Server: data.Server, + Port: data.Port, + UUID: uuid, + Flow: vless.Flow, + } + setSecurityOptions(p, vless.Security, vless.SecurityConfig) + clashTransport(p, vless.Transport, vless.TransportConfig) + return p, nil +} + +func parseVmess(data proxy.Proxy, uuid string) (*Proxy, error) { + vmess, ok := data.Option.(proxy.Vmess) + if !ok { + return nil, fmt.Errorf("invalid type for Vmess") + } + alterID := 0 + p := &Proxy{ + Name: data.Name, + Type: "vmess", + Server: data.Server, + Port: data.Port, + UUID: uuid, + AlterID: &alterID, + Cipher: "auto", + } + setSecurityOptions(p, vmess.Security, vmess.SecurityConfig) + clashTransport(p, vmess.Transport, vmess.TransportConfig) + return p, nil +} + +func parseHysteria2(data proxy.Proxy, uuid string) (*Proxy, error) { + hysteria2, ok := data.Option.(proxy.Hysteria2) + if !ok { + return nil, fmt.Errorf("invalid type for Hysteria2") + } + p := &Proxy{ + Name: data.Name, + Type: "hysteria2", + Server: data.Server, + Port: data.Port, + Ports: hysteria2.HopPorts, + Password: uuid, + HeartbeatInterval: hysteria2.HopInterval, + SkipCertVerify: hysteria2.SecurityConfig.AllowInsecure, + SNI: hysteria2.SecurityConfig.SNI, + } + if hysteria2.ObfsPassword != "" { + p.Obfs = "salamander" + p.ObfsPassword = hysteria2.ObfsPassword + } + + return p, nil +} + +func parseTuic(data proxy.Proxy, uuid string) (*Proxy, error) { + tuic, ok := data.Option.(proxy.Tuic) + if !ok { + return nil, fmt.Errorf("invalid type for Tuic") + } + p := &Proxy{ + Name: data.Name, + Type: "tuic", + Server: data.Server, + Port: data.Port, + UUID: uuid, + Password: uuid, + SNI: tuic.SecurityConfig.SNI, + SkipCertVerify: tuic.SecurityConfig.AllowInsecure, + } + + return p, nil +} + +func setSecurityOptions(p *Proxy, security string, config proxy.SecurityConfig) { + switch security { + case "tls": + p.TLS = true + p.ServerName = config.SNI + p.ClientFingerprint = config.Fingerprint + p.SkipCertVerify = config.AllowInsecure + case "reality": + p.TLS = true + p.ServerName = config.SNI + p.ClientFingerprint = config.Fingerprint + p.RealityOpts = RealityOptions{ + PublicKey: config.RealityPublicKey, + ShortID: config.RealityShortId, + } + p.SkipCertVerify = config.AllowInsecure + default: + p.TLS = false + } +} + +func setTransportOptions(p *Proxy, transport string, config proxy.TransportConfig) { + switch transport { + case "websocket": + p.Network = "ws" + p.WSOpts = WSOptions{ + Path: config.Path, + Headers: map[string]string{ + "Host": config.Host, + }, + } + case "grpc": + p.Network = "grpc" + p.GrpcOpts = GrpcOptions{ + GrpcServiceName: config.ServiceName, + } + default: + p.Network = "tcp" + } +} diff --git a/pkg/adapter/clash/tool.go b/pkg/adapter/clash/tool.go new file mode 100644 index 0000000..e4c989e --- /dev/null +++ b/pkg/adapter/clash/tool.go @@ -0,0 +1,33 @@ +package clash + +import "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" + +func clashTransport(c *Proxy, transportType string, transportConfig proxy.TransportConfig) { + + switch transportType { + case "websocket", "httpupgrade": + if transportType == "websocket" { + c.Network = "ws" + } else { + c.Network = transportType + } + c.WSOpts = WSOptions{ + Path: transportConfig.Path, + Headers: map[string]string{}, + } + if transportConfig.Host != "" { + c.WSOpts.Headers["host"] = transportConfig.Host + } + if transportType == "httpupgrade" { + c.WSOpts.V2rayHttpUpgrade = true + } + case "grpc": + c.Network = "grpc" + c.GrpcOpts = GrpcOptions{ + GrpcServiceName: transportConfig.ServiceName, + } + case "tcp": + c.Network = "tcp" + } + +} diff --git a/pkg/adapter/general/uri.go b/pkg/adapter/general/uri.go new file mode 100644 index 0000000..a5871fb --- /dev/null +++ b/pkg/adapter/general/uri.go @@ -0,0 +1,245 @@ +package general + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "net" + "net/url" + "strconv" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +type v2rayShareLink struct { + Ps string `json:"ps"` + Add string `json:"add"` + Port string `json:"port"` + ID string `json:"id"` + Aid string `json:"aid"` + Net string `json:"net"` + Type string `json:"type"` + Host string `json:"host"` + SNI string `json:"sni"` + Path string `json:"path"` + TLS string `json:"tls"` + Flow string `json:"flow,omitempty"` + Alpn string `json:"alpn,omitempty"` + AllowInsecure bool `json:"allowInsecure"` + Fingerprint string `json:"fp,omitempty"` + PublicKey string `json:"pbk,omitempty"` + ShortId string `json:"sid,omitempty"` + SpiderX string `json:"spx,omitempty"` + V string `json:"v"` +} + +// GenerateBase64General will output node URLs split by '\n' and then encode into base64 +func GenerateBase64General(data []proxy.Proxy, uuid string) []byte { + var links []string + for _, v := range data { + p := buildProxy(v, uuid) + if p == "" { + continue + } + links = append(links, p) + } + var rsp []byte + rsp = base64.RawStdEncoding.AppendEncode(rsp, []byte(strings.Join(links, "\n"))) + return rsp +} + +func buildProxy(data proxy.Proxy, uuid string) string { + switch data.Protocol { + case "shadowsocks": + return ShadowsocksUri(data, uuid) + case "vmess": + return VmessUri(data, uuid) + case "vless": + return VlessUri(data, uuid) + case "trojan": + return TrojanUri(data, uuid) + case "hysteria2": + return Hysteria2Uri(data, uuid) + case "tuic": + return TuicUri(data, uuid) + default: + return "" + } +} + +func ShadowsocksUri(data proxy.Proxy, uuid string) string { + ss := data.Option.(proxy.Shadowsocks) + // sip002 + u := &url.URL{ + Scheme: "ss", + // 还没有写 2022 的 + User: url.User(strings.TrimSuffix(base64.URLEncoding.EncodeToString([]byte(ss.Method+":"+uuid)), "=")), + Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)), + Fragment: data.Name, + } + return u.String() +} + +func VmessUri(data proxy.Proxy, uuid string) string { + vmess := data.Option.(proxy.Vmess) + + transport := vmess.TransportConfig + + securityConfig := vmess.SecurityConfig + + var s = v2rayShareLink{ + V: "2", + Add: data.Server, + Port: fmt.Sprint(data.Port), + ID: uuid, + Aid: "0", + Net: vmess.Transport, + // Type: "?", + Host: transport.Host, + Path: transport.Path, + } + + if vmess.Security == "tls" { + s.TLS = "tls" + s.SNI = securityConfig.SNI + s.AllowInsecure = securityConfig.AllowInsecure + s.Fingerprint = securityConfig.Fingerprint + } + b, _ := json.Marshal(s) + return "vmess://" + strings.TrimSuffix(base64.StdEncoding.EncodeToString(b), "=") +} + +func VlessUri(data proxy.Proxy, uuid string) string { + vless := data.Option.(proxy.Vless) + transportConfig := vless.TransportConfig + securityConfig := vless.SecurityConfig + + var query = make(url.Values) + setQuery(&query, "flow", vless.Flow) + setQuery(&query, "type", vless.Transport) + setQuery(&query, "security", vless.Security) + + switch vless.Transport { + case "ws", "http", "httpupgrade": + setQuery(&query, "path", transportConfig.Path) + setQuery(&query, "host", transportConfig.Host) + case "grpc": + setQuery(&query, "serviceName", transportConfig.ServiceName) + case "meek": + setQuery(&query, "url", transportConfig.Host) + } + + setQuery(&query, "sni", securityConfig.SNI) + setQuery(&query, "fp", securityConfig.Fingerprint) + setQuery(&query, "pbk", securityConfig.RealityPublicKey) + setQuery(&query, "sid", securityConfig.RealityShortId) + + u := url.URL{ + Scheme: "vless", + User: url.User(uuid), + Host: net.JoinHostPort(data.Server, fmt.Sprint(data.Port)), + RawQuery: query.Encode(), + Fragment: data.Name, + } + return u.String() +} + +func TrojanUri(data proxy.Proxy, uuid string) string { + trojan := data.Option.(proxy.Trojan) + transportConfig := trojan.TransportConfig + securityConfig := trojan.SecurityConfig + + var query = make(url.Values) + setQuery(&query, "type", trojan.Transport) + setQuery(&query, "security", trojan.Security) + + switch trojan.Transport { + case "ws", "http", "httpupgrade": + setQuery(&query, "path", transportConfig.Path) + setQuery(&query, "host", transportConfig.Host) + case "grpc": + setQuery(&query, "serviceName", transportConfig.ServiceName) + case "meek": + setQuery(&query, "url", transportConfig.Host) + } + + setQuery(&query, "sni", securityConfig.SNI) + setQuery(&query, "fp", securityConfig.Fingerprint) + setQuery(&query, "pbk", securityConfig.RealityPublicKey) + setQuery(&query, "sid", securityConfig.RealityShortId) + + if securityConfig.AllowInsecure { + setQuery(&query, "allowInsecure", "1") + } + + u := &url.URL{ + Scheme: "trojan", + User: url.User(uuid), + Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)), + RawQuery: query.Encode(), + Fragment: data.Name, + } + return u.String() +} + +func Hysteria2Uri(data proxy.Proxy, uuid string) string { + hysteria2 := data.Option.(proxy.Hysteria2) + + var query = make(url.Values) + + setQuery(&query, "sni", hysteria2.SecurityConfig.SNI) + + if hysteria2.SecurityConfig.AllowInsecure { + setQuery(&query, "insecure", "1") + } + + if hp := strings.TrimSpace(hysteria2.HopPorts); hp != "" { + setQuery(&query, "mport", hp) + } + + if hysteria2.ObfsPassword != "" { + setQuery(&query, "obfs", "salamander") + setQuery(&query, "obfs-password", hysteria2.ObfsPassword) + } + + u := &url.URL{ + Scheme: "hysteria2", + User: url.User(uuid), + Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)), + RawQuery: query.Encode(), + Fragment: data.Name, + } + return u.String() +} + +func TuicUri(data proxy.Proxy, uuid string) string { + tuic := data.Option.(proxy.Tuic) + var query = make(url.Values) + + setQuery(&query, "congestion_control", "bbr") + + if tuic.SecurityConfig.SNI == "" { + setQuery(&query, "sni", tuic.SecurityConfig.SNI) + } else { + setQuery(&query, "disable_sni", "1") + } + if tuic.SecurityConfig.AllowInsecure { + setQuery(&query, "allow_insecure", "1") + } + + u := &url.URL{ + Scheme: "tuic", + User: url.User(uuid + ":" + uuid), + Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)), + RawQuery: query.Encode(), + Fragment: data.Name, + } + return u.String() +} + +func setQuery(q *url.Values, k, v string) { + if v != "" { + q.Set(k, v) + } +} diff --git a/pkg/adapter/general/uri_test.go b/pkg/adapter/general/uri_test.go new file mode 100644 index 0000000..a33ef10 --- /dev/null +++ b/pkg/adapter/general/uri_test.go @@ -0,0 +1,26 @@ +package general + +import ( + "testing" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func createServer() proxy.Proxy { + return proxy.Proxy{ + Name: "Meta", + Server: "127.0.0.1", + Port: 13092, + Protocol: "shadowsocks", + Option: proxy.Shadowsocks{ + Method: "aes-256-gcm", + ServerKey: "", + }, + } +} + +func TestGenerateBase64General(t *testing.T) { + s := createServer() + p := buildProxy(s, "935b33c7-e128-49f2-816b-71070469cac2") + t.Log(p) +} diff --git a/pkg/adapter/loon/build.go b/pkg/adapter/loon/build.go new file mode 100644 index 0000000..3256385 --- /dev/null +++ b/pkg/adapter/loon/build.go @@ -0,0 +1,27 @@ +package loon + +import ( + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func BuildLoon(servers []proxy.Proxy, uuid string) []byte { + uri := "" + for _, s := range servers { + switch s.Protocol { + case "vmess": + uri += buildVMess(s, uuid) + case "shadowsocks": + uri += buildShadowsocks(s, uuid) + case "trojan": + uri += buildTrojan(s, uuid) + case "vless": + uri += buildVless(s, uuid) + case "hysteria2": + uri += buildHysteria2(s, uuid) + default: + continue + } + } + + return []byte(uri) +} diff --git a/pkg/adapter/loon/hysteria2.go b/pkg/adapter/loon/hysteria2.go new file mode 100644 index 0000000..5b1547f --- /dev/null +++ b/pkg/adapter/loon/hysteria2.go @@ -0,0 +1,34 @@ +package loon + +import ( + "fmt" + "strconv" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func buildHysteria2(data proxy.Proxy, password string) string { + hysteria2 := data.Option.(proxy.Hysteria2) + + configs := []string{ + fmt.Sprintf("%s=Hysteria2", data.Name), + data.Server, + strconv.Itoa(data.Port), + password, + "udp=true", + } + if hysteria2.ObfsPassword != "" { + configs = append(configs, "obfs=salamander", fmt.Sprintf("salamander-password=%s", hysteria2.ObfsPassword)) + } + if hysteria2.SecurityConfig.SNI != "" { + configs = append(configs, fmt.Sprintf("sni=%s", hysteria2.SecurityConfig.SNI)) + if hysteria2.SecurityConfig.AllowInsecure { + configs = append(configs, "skip-cert-verify=true") + } else { + configs = append(configs, "skip-cert-verify=false") + } + } + uri := strings.Join(configs, ",") + return uri + "\r\n" +} diff --git a/pkg/adapter/loon/loon_test.go b/pkg/adapter/loon/loon_test.go new file mode 100644 index 0000000..281cd5a --- /dev/null +++ b/pkg/adapter/loon/loon_test.go @@ -0,0 +1,29 @@ +package loon + +import ( + "testing" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func createSS() proxy.Proxy { + return proxy.Proxy{ + Name: "Shadowsocks", + Server: "127.0.0.1", + Port: 10301, + Protocol: "shadowsocks", + Option: proxy.Shadowsocks{ + Method: "aes-256-gcm", + ServerKey: "", + }, + } + +} + +func TestBuildSS(t *testing.T) { + s := createSS() + + password := "f0d0237d-193a-4cf5-99dd-b02207beaea6" + uri := buildShadowsocks(s, password) + t.Log(uri) +} diff --git a/pkg/adapter/loon/shadowsocks.go b/pkg/adapter/loon/shadowsocks.go new file mode 100644 index 0000000..ffd7494 --- /dev/null +++ b/pkg/adapter/loon/shadowsocks.go @@ -0,0 +1,49 @@ +package loon + +import ( + "fmt" + "strconv" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/uuidx" +) + +func buildShadowsocks(data proxy.Proxy, password string) string { + shadowsocks := data.Option.(proxy.Shadowsocks) + // If the method is 2022-blake3-chacha20-poly1305, it means that the server is a relay server + if shadowsocks.Method == "2022-blake3-chacha20-poly1305" { + return "" + } + + if strings.Contains(shadowsocks.Method, "2022") { + serverKey, userKey := generateShadowsocks2022Password(shadowsocks, password) + password = fmt.Sprintf("%s:%s", serverKey, userKey) + } + + configs := []string{ + fmt.Sprintf("%s=Shadowsocks", data.Name), + data.Server, + strconv.Itoa(data.Port), + shadowsocks.Method, + password, + "fast-open=false", + "udp=true", + } + uri := strings.Join(configs, ",") + return uri + "\r\n" +} + +func generateShadowsocks2022Password(ss proxy.Shadowsocks, password string) (string, string) { + // server key + var serverKey string + if ss.Method == "2022-blake3-aes-128-gcm" { + serverKey = tool.GenerateCipher(ss.ServerKey, 16) + password = uuidx.UUIDToBase64(password, 16) + } else { + serverKey = tool.GenerateCipher(ss.ServerKey, 32) + password = uuidx.UUIDToBase64(password, 32) + } + return serverKey, password +} diff --git a/pkg/adapter/loon/trojan.go b/pkg/adapter/loon/trojan.go new file mode 100644 index 0000000..3017d91 --- /dev/null +++ b/pkg/adapter/loon/trojan.go @@ -0,0 +1,44 @@ +package loon + +import ( + "fmt" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func buildTrojan(data proxy.Proxy, password string) string { + trojan := data.Option.(proxy.Trojan) + + configs := []string{ + fmt.Sprintf("%s=trojan", data.Name), + data.Server, + fmt.Sprintf("%d", data.Port), + "auto", + password, + "fast-open=false", + "udp=true", + } + + if trojan.SecurityConfig.SNI != "" { + configs = append(configs, fmt.Sprintf("sni=%s", trojan.SecurityConfig.SNI)) + } + if trojan.SecurityConfig.AllowInsecure { + configs = append(configs, "skip-cert-verify=true") + } else { + configs = append(configs, "skip-cert-verify=false") + } + + if trojan.Transport == "websocket" { + configs = append(configs, "transport=ws") + if trojan.TransportConfig.Path != "" { + configs = append(configs, fmt.Sprintf("path=%s", trojan.TransportConfig.Path)) + } + if trojan.TransportConfig.Host != "" { + configs = append(configs, fmt.Sprintf("host=%s", trojan.TransportConfig.Host)) + } + } + + uri := strings.Join(configs, ",") + return uri + "\r\n" +} diff --git a/pkg/adapter/loon/vless.go b/pkg/adapter/loon/vless.go new file mode 100644 index 0000000..f14bbcb --- /dev/null +++ b/pkg/adapter/loon/vless.go @@ -0,0 +1,62 @@ +package loon + +import ( + "fmt" + "strconv" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" + + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +func buildVless(data proxy.Proxy, password string) string { + vless := data.Option.(proxy.Vless) + // If flow is not empty, it means that the server is a relay server + if vless.Flow != "" { + return "" + } + + configs := []string{ + fmt.Sprintf("%s=vless", data.Name), + data.Server, + strconv.Itoa(data.Port), + "auto", + password, + "fast-open=false", + "udp=true", + "alterId=0", + } + + switch vless.Transport { + case "tcp": + configs = append(configs, "transport=tcp") + case "websocket": + configs = append(configs, "transport=ws") + if vless.TransportConfig.Path != "" { + configs = append(configs, fmt.Sprintf("path=%s", vless.TransportConfig.Path)) + } + if vless.TransportConfig.Host != "" { + configs = append(configs, fmt.Sprintf("host=%s", vless.TransportConfig.Host)) + } + default: + logger.Info("Loon Unknown transport type: ", logger.Field("transport", vless.Transport)) + return "" + } + + if vless.Security == "tls" { + configs = append(configs, "over-tls=true", fmt.Sprintf("tls-name=%s", vless.SecurityConfig.SNI)) + if vless.SecurityConfig.AllowInsecure { + configs = append(configs, "skip-cert-verify=true") + } else { + configs = append(configs, "skip-cert-verify=false") + } + } else if vless.Security == "reality" { + // Loon does not support reality security + logger.Info("Loon Unknown security type: ", logger.Field("security", vless.Security)) + return "" + } + + uri := strings.Join(configs, ",") + return uri + "\r\n" +} diff --git a/pkg/adapter/loon/vmess.go b/pkg/adapter/loon/vmess.go new file mode 100644 index 0000000..5d693b1 --- /dev/null +++ b/pkg/adapter/loon/vmess.go @@ -0,0 +1,53 @@ +package loon + +import ( + "fmt" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +func buildVMess(data proxy.Proxy, password string) string { + vmess := data.Option.(proxy.Vmess) + + configs := []string{ + fmt.Sprintf("%s=vmess", data.Name), + data.Server, + fmt.Sprintf("%d", data.Port), + "auto", + password, + "fast-open=false", + "udp=true", + "alterId=0", + } + + switch vmess.Transport { + case "tcp": + configs = append(configs, "transport=tcp") + case "websocket": + configs = append(configs, "transport=ws") + if vmess.TransportConfig.Path != "" { + configs = append(configs, fmt.Sprintf("path=%s", vmess.TransportConfig.Path)) + } + if vmess.TransportConfig.Host != "" { + configs = append(configs, fmt.Sprintf("host=%s", vmess.TransportConfig.Host)) + } + default: + logger.Info("Loon Unknown transport type: ", logger.Field("transport", vmess.Transport)) + return "" + } + + if vmess.Security == "tls" { + configs = append(configs, "over-tls=true", fmt.Sprintf("tls-name=%s", vmess.SecurityConfig.SNI)) + if vmess.SecurityConfig.AllowInsecure { + configs = append(configs, "skip-cert-verify=true") + } else { + configs = append(configs, "skip-cert-verify=false") + } + + } + + uri := strings.Join(configs, ",") + return uri + "\r\n" +} diff --git a/pkg/adapter/proxy/proxy.go b/pkg/adapter/proxy/proxy.go new file mode 100644 index 0000000..d2cf603 --- /dev/null +++ b/pkg/adapter/proxy/proxy.go @@ -0,0 +1,114 @@ +package proxy + +// Adapter represents a proxy adapter +type Adapter struct { + Proxies []Proxy + Group []Group + Rules []string + Region []string +} + +// Proxy represents a proxy server +type Proxy struct { + Name string + Server string + Port int + Protocol string + Country string + Option any +} + +// Group represents a group of proxies +type Group struct { + Name string + Type GroupType + Proxies []string + URL string + Interval int +} + +type GroupType string + +const ( + GroupTypeSelect GroupType = "select" + GroupTypeURLTest GroupType = "url-test" + GroupTypeFallback GroupType = "fallback" +) + +// Shadowsocks represents a Shadowsocks proxy configuration +type Shadowsocks struct { + Port int `json:"port"` + Method string `json:"method"` + ServerKey string `json:"server_key"` +} + +// Vless represents a Vless proxy configuration +type Vless struct { + Port int `json:"port"` + Flow string `json:"flow"` + Transport string `json:"transport"` + TransportConfig TransportConfig `json:"transport_config"` + Security string `json:"security"` + SecurityConfig SecurityConfig `json:"security_config"` +} + +// Vmess represents a Vmess proxy configuration +type Vmess struct { + Port int `json:"port"` + Flow string `json:"flow"` + Transport string `json:"transport"` + TransportConfig TransportConfig `json:"transport_config"` + Security string `json:"security"` + SecurityConfig SecurityConfig `json:"security_config"` +} + +// Trojan represents a Trojan proxy configuration +type Trojan struct { + Port int `json:"port"` + Flow string `json:"flow"` + Transport string `json:"transport"` + TransportConfig TransportConfig `json:"transport_config"` + Security string `json:"security"` + SecurityConfig SecurityConfig `json:"security_config"` +} + +// Hysteria2 represents a Hysteria2 proxy configuration +type Hysteria2 struct { + Port int `json:"port"` + HopPorts string `json:"hop_ports"` + HopInterval int `json:"hop_interval"` + ObfsPassword string `json:"obfs_password"` + SecurityConfig SecurityConfig `json:"security_config"` +} + +// Tuic represents a Tuic proxy configuration +type Tuic struct { + Port int `json:"port"` + SecurityConfig SecurityConfig `json:"security_config"` +} + +// TransportConfig represents the transport configuration for a proxy +type TransportConfig struct { + Path string `json:"path,omitempty"` // ws/httpupgrade + Host string `json:"host,omitempty"` + ServiceName string `json:"service_name"` // grpc +} + +// SecurityConfig represents the security configuration for a proxy +type SecurityConfig struct { + SNI string `json:"sni"` + AllowInsecure bool `json:"allow_insecure"` + Fingerprint string `json:"fingerprint"` + RealityServerAddr string `json:"reality_server_addr"` + RealityServerPort int `json:"reality_server_port"` + RealityPrivateKey string `json:"reality_private_key"` + RealityPublicKey string `json:"reality_public_key"` + RealityShortId string `json:"reality_short_id"` +} + +// Relay represents a relay configuration +type Relay struct { + RelayHost string + DispatchMode string + Prefix string +} diff --git a/pkg/adapter/quantumultx/build.go b/pkg/adapter/quantumultx/build.go new file mode 100644 index 0000000..a5b02cd --- /dev/null +++ b/pkg/adapter/quantumultx/build.go @@ -0,0 +1,22 @@ +package quantumultx + +import ( + "encoding/base64" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func BuildQuantumultX(servers []proxy.Proxy, uuid string) string { + var uri string + for _, s := range servers { + switch s.Protocol { + case "vmess": + uri += buildVmess(s, uuid) + case "shadowsocks": + uri += buildShadowsocks(s, uuid) + case "trojan": + uri += buildTrojan(s, uuid) + } + } + return base64.StdEncoding.EncodeToString([]byte(uri)) +} diff --git a/pkg/adapter/quantumultx/quantumux_test.go b/pkg/adapter/quantumultx/quantumux_test.go new file mode 100644 index 0000000..79524bb --- /dev/null +++ b/pkg/adapter/quantumultx/quantumux_test.go @@ -0,0 +1,94 @@ +package quantumultx + +import ( + "testing" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func createVMess() proxy.Proxy { + + return proxy.Proxy{ + Name: "Vmess", + Server: "test.xxxx.com", + Port: 13002, + Protocol: "vmess", + Option: proxy.Vmess{ + Port: 13002, + Transport: "websocket", + TransportConfig: proxy.TransportConfig{ + Path: "/ws", + Host: "test.xx.com", + }, + Security: "none", + }, + } +} + +func createSS() proxy.Proxy { + return proxy.Proxy{ + Name: "Shadowsocks", + Server: "test.xxxx.com", + Port: 10301, + Protocol: "shadowsocks", + Option: proxy.Shadowsocks{ + Port: 10301, + Method: "aes-256-gcm", + ServerKey: "123456", + }, + } +} + +func createTrojan() proxy.Proxy { + + return proxy.Proxy{ + Name: "Trojan", + Server: "test.xxxx.com", + Port: 13002, + Protocol: "trojan", + Option: proxy.Trojan{ + Port: 13002, + Transport: "websocket", + TransportConfig: proxy.TransportConfig{ + Path: "/ws", + Host: "baidu.com", + }, + SecurityConfig: proxy.SecurityConfig{ + SNI: "baidu.com", + AllowInsecure: true, + }, + }, + } +} +func TestVmess(t *testing.T) { + s := createVMess() + vmess := buildVmess(s, "uuid") + t.Log(vmess) + // output: + // vmess=127.0.0.1:13002,method=chacha20-poly1305,password=uuid,fast-open=true,udp-relay=true,tag=Vmess,tls-verification=true,obfs-uri=/ws,obfs-host=baidu.com +} + +func TestShadowsocks(t *testing.T) { + s := createSS() + shadowsocks := buildShadowsocks(s, "uuid") + t.Log(shadowsocks) + // output: + // shadowsocks=127.0.0.1:10301,method=aes-256-gcm,password=uuid,fast-open=true,udp-relay=true,tag=Shadowsocks +} + +func TestTrojan(t *testing.T) { + s := createTrojan() + trojan := buildTrojan(s, "password") + t.Log(trojan) + // output: + // trojan=192.168.0.1:13002,password=password,fast-open=true,udp-relay=true,tag=Trojan,obfs=wss,obfs-uri=ws,obfs-host=baidu.com +} + +func TestBuildQuantumultX(t *testing.T) { + var servers []proxy.Proxy + uri := BuildQuantumultX(servers, "uuid") + t.Log(uri) + + // output: + // c2hhZG93c29ja3M9MTI3LjAuMC4xOjEwMzAxLG1ldGhvZD1hZXMtMjU2LWdjbSxwYXNzd29yZD11dWlkLGZhc3Qtb3Blbj10cnVlLHVkcC1yZWxheT10cnVlLHRhZz1TaGFkb3dzb2Nrcw0KdHJvamFuPTE5Mi4xNjguMC4xOjEzMDAyLHBhc3N3b3JkPXV1aWQsZmFzdC1vcGVuPXRydWUsdWRwLXJlbGF5PXRydWUsdGFnPVRyb2phbixvYmZzPXdzcyxvYmZzLXVyaT13cyxvYmZzLWhvc3Q9YmFpZHUuY29tDQp2bWVzcz0xMjcuMC4wLjE6MTMwMDIsbWV0aG9kPWNoYWNoYTIwLXBvbHkxMzA1LHBhc3N3b3JkPXV1aWQsZmFzdC1vcGVuPXRydWUsdWRwLXJlbGF5PXRydWUsdGFnPVZtZXNzLHRscy12ZXJpZmljYXRpb249dHJ1ZSxvYmZzLXVyaT0vd3Msb2Jmcy1ob3N0PWJhaWR1LmNvbQ0K +} diff --git a/pkg/adapter/quantumultx/shadowsocks.go b/pkg/adapter/quantumultx/shadowsocks.go new file mode 100644 index 0000000..a03703c --- /dev/null +++ b/pkg/adapter/quantumultx/shadowsocks.go @@ -0,0 +1,23 @@ +package quantumultx + +import ( + "fmt" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func buildShadowsocks(data proxy.Proxy, uuid string) string { + ss := data.Option.(proxy.Shadowsocks) + addr := fmt.Sprintf("%s:%d", data.Server, data.Port) + + config := []string{ + addr, + fmt.Sprintf("method=%s", ss.Method), + fmt.Sprintf("password=%s", uuid), + "fast-open=true", + "udp-relay=true", + fmt.Sprintf("tag=%s", data.Name), + } + return strings.Join(config, ",") + "\r\n" +} diff --git a/pkg/adapter/quantumultx/trojan.go b/pkg/adapter/quantumultx/trojan.go new file mode 100644 index 0000000..0ebcd96 --- /dev/null +++ b/pkg/adapter/quantumultx/trojan.go @@ -0,0 +1,39 @@ +package quantumultx + +import ( + "fmt" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +// 生成 Trojan 配置 +func buildTrojan(data proxy.Proxy, password string) string { + trojan := data.Option.(proxy.Trojan) + + addr := fmt.Sprintf("trojan=%s:%d", data.Server, data.Port) + config := []string{ + addr, + fmt.Sprintf("password=%s", password), + "fast-open=true", + "udp-relay=true", + fmt.Sprintf("tag=%s", data.Name), + } + + if trojan.Transport == "websocket" { + config = append(config, "obfs=wss") + if trojan.TransportConfig.Path != "" { + config = append(config, fmt.Sprintf("obfs-uri=%s", trojan.TransportConfig.Path)) + } + if trojan.TransportConfig.Host != "" { + config = append(config, fmt.Sprintf("obfs-host=%s", trojan.TransportConfig.Host)) + } + } else { + config = append(config, "over-tls=true") + if trojan.SecurityConfig.SNI != "" { + config = append(config, fmt.Sprintf("tls-host=%s", trojan.SecurityConfig.SNI)) + } + } + + return strings.Join(config, ",") + "\r\n" +} diff --git a/pkg/adapter/quantumultx/vmess.go b/pkg/adapter/quantumultx/vmess.go new file mode 100644 index 0000000..d3cf13e --- /dev/null +++ b/pkg/adapter/quantumultx/vmess.go @@ -0,0 +1,45 @@ +package quantumultx + +import ( + "fmt" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func buildVmess(data proxy.Proxy, uuid string) string { + + vmess := data.Option.(proxy.Vmess) + addr := fmt.Sprintf("vmess=%s:%d", data.Server, data.Port) + var host string + uriConfig := []string{ + addr, + "method=chacha20-poly1305", + fmt.Sprintf("password=%s", uuid), + "fast-open=true", + "udp-relay=true", + fmt.Sprintf("tag=%s", data.Name), + } + if vmess.Security == "tls" { + if vmess.Transport == "tcp" { + uriConfig = append(uriConfig, "obfs=over-tls") + } + if vmess.SecurityConfig.AllowInsecure { + uriConfig = append(uriConfig, "tls-verification=true") + } else { + uriConfig = append(uriConfig, "tls-verification=false") + } + if vmess.SecurityConfig.SNI != "" { + host = vmess.SecurityConfig.SNI + } + } + + if vmess.Transport == "websocket" { + uriConfig = append(uriConfig, fmt.Sprintf("obfs-uri=%s", vmess.TransportConfig.Path)) + host = vmess.TransportConfig.Host + } + if host != "" { + uriConfig = append(uriConfig, fmt.Sprintf("obfs-host=%s", host)) + } + return strings.Join(uriConfig, ",") + "\r\n" +} diff --git a/pkg/adapter/shadowrocket/build.go b/pkg/adapter/shadowrocket/build.go new file mode 100644 index 0000000..33143a7 --- /dev/null +++ b/pkg/adapter/shadowrocket/build.go @@ -0,0 +1,48 @@ +package shadowrocket + +import ( + "fmt" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/general" + + "encoding/base64" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" + "github.com/perfect-panel/ppanel-server/pkg/traffic" +) + +type UserInfo struct { + Upload int64 + Download int64 + TotalTraffic int64 + ExpiredDate time.Time +} + +func BuildShadowrocket(servers []proxy.Proxy, uuid string, userinfo UserInfo) []byte { + upload := traffic.AutoConvert(userinfo.Upload, false) + download := traffic.AutoConvert(userinfo.Download, false) + total := traffic.AutoConvert(userinfo.TotalTraffic, false) + expiredAt := userinfo.ExpiredDate.Format("2006-01-02 15:04:05") + uri := fmt.Sprintf("STATUS=🚀↑:%s,↓:%s,TOT:%s💡Expires:%s\r\n", upload, download, total, expiredAt) + for _, s := range servers { + switch s.Protocol { + case "vmess": + uri += buildVmess(s, uuid) + case "shadowsocks": + uri += general.ShadowsocksUri(s, uuid) + "\r\n" + case "trojan": + uri += general.TrojanUri(s, uuid) + "\r\n" + case "vless": + uri += general.VlessUri(s, uuid) + "\r\n" + case "hysteria2": + uri += general.Hysteria2Uri(s, uuid) + "\r\n" + case "tuic": + uri += general.TuicUri(s, uuid) + "\r\n" + default: + continue + } + } + + return []byte(base64.StdEncoding.EncodeToString([]byte(uri))) +} diff --git a/pkg/adapter/shadowrocket/shadowrocket_test.go b/pkg/adapter/shadowrocket/shadowrocket_test.go new file mode 100644 index 0000000..50b8932 --- /dev/null +++ b/pkg/adapter/shadowrocket/shadowrocket_test.go @@ -0,0 +1,76 @@ +package shadowrocket + +import ( + "testing" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func createVMess() proxy.Proxy { + return proxy.Proxy{ + Name: "Vmess", + Server: "test.xxxx.com", + Port: 13002, + Protocol: "vmess", + Option: proxy.Vmess{ + Port: 13002, + Transport: "websocket", + TransportConfig: proxy.TransportConfig{ + Path: "/ws", + Host: "test.xx.com", + }, + Security: "none", + }, + } +} + +func createSS() proxy.Proxy { + return proxy.Proxy{ + Name: "Shadowsocks", + Server: "test.xxxx.com", + Port: 10301, + Protocol: "shadowsocks", + Option: proxy.Shadowsocks{ + Port: 10301, + Method: "aes-256-gcm", + ServerKey: "123456", + }, + } +} + +func createTrojan() proxy.Proxy { + + return proxy.Proxy{ + Name: "Trojan", + Server: "test.xxxx.com", + Port: 13002, + Protocol: "trojan", + Option: proxy.Trojan{ + Port: 13002, + Transport: "websocket", + TransportConfig: proxy.TransportConfig{ + Path: "/ws", + Host: "baidu.com", + }, + SecurityConfig: proxy.SecurityConfig{ + SNI: "baidu.com", + AllowInsecure: true, + }, + }, + } +} +func TestBuildShadowrocket(t *testing.T) { + s := []proxy.Proxy{ + createVMess(), + createSS(), + createTrojan(), + } + uri := BuildShadowrocket(s, "uuid", UserInfo{ + Upload: 1024, + Download: 1024, + TotalTraffic: 2048, + ExpiredDate: time.Now().AddDate(0, 0, 1), + }) + t.Log(string(uri)) +} diff --git a/pkg/adapter/shadowrocket/vmess.go b/pkg/adapter/shadowrocket/vmess.go new file mode 100644 index 0000000..f309566 --- /dev/null +++ b/pkg/adapter/shadowrocket/vmess.go @@ -0,0 +1,57 @@ +package shadowrocket + +import ( + "fmt" + "strings" + + "encoding/base64" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func buildVmess(data proxy.Proxy, uuid string) string { + vmess := data.Option.(proxy.Vmess) + + userinfo := fmt.Sprintf("auto:%s@%s:%d", uuid, data.Server, data.Port) + // 准备 config,使用默认值 + config := map[string]interface{}{ + "tfo": 1, + "remark": data.Name, + "alterId": 0, + } + + // tls 配置 + if vmess.Security == "tls" { + config["tls"] = 1 + if vmess.SecurityConfig.AllowInsecure { + config["allowInsecure"] = 1 + } + if vmess.SecurityConfig.SNI != "" { + config["peer"] = vmess.SecurityConfig.SNI + } + } + + // transport 配置 + switch vmess.Transport { + case "websocket": + config["obfs"] = "websocket" + if vmess.TransportConfig.Path != "" { + config["path"] = vmess.TransportConfig.Path + } + if vmess.TransportConfig.Host != "" { + config["obfsParam"] = vmess.TransportConfig.Host + } + case "grpc": + config["obfs"] = "grpc" + if vmess.TransportConfig.ServiceName != "" { + config["path"] = vmess.TransportConfig.ServiceName + } + } + query := make([]string, 0) + for k, v := range config { + query = append(query, fmt.Sprintf("%s=%v", k, v)) + } + queryStr := strings.Join(query, "&") + uri := fmt.Sprintf("vmess://%s?%s\r\n", base64.StdEncoding.EncodeToString([]byte(userinfo)), queryStr) + return uri +} diff --git a/pkg/adapter/singbox/build.go b/pkg/adapter/singbox/build.go new file mode 100644 index 0000000..30fbd61 --- /dev/null +++ b/pkg/adapter/singbox/build.go @@ -0,0 +1,201 @@ +package singbox + +import ( + "encoding/json" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +func BuildSingbox(adapter proxy.Adapter, uuid string) ([]byte, error) { + // build outbounds type is Proxy + var proxies []Proxy + // build outbound group + for _, group := range adapter.Group { + if group.Type == proxy.GroupTypeSelect { + selector := Proxy{ + Type: Selector, + Tag: group.Name, + SelectorOptions: &SelectorOutboundOptions{ + OutboundOptions: OutboundOptions{ + Tag: group.Name, + Type: Selector, + }, + Outbounds: group.Proxies, + Default: group.Proxies[0], + InterruptExistConnections: false, + }, + } + proxies = append(proxies, selector) + } else if group.Type == proxy.GroupTypeURLTest { + selector := Proxy{ + Type: URLTest, + Tag: group.Name, + URLTestOptions: &URLTestOutboundOptions{ + OutboundOptions: OutboundOptions{ + Tag: group.Name, + Type: URLTest, + }, + Outbounds: group.Proxies, + URL: group.URL, + }, + } + proxies = append(proxies, selector) + } else { + logger.Errorf("[sing-box] Unknown group type: %s, group name: %s", group.Type, group.Name) + } + } + + // build outbounds + for _, data := range adapter.Proxies { + p := buildProxy(data, uuid) + if p == nil { + continue + } + proxies = append(proxies, *p) + } + + // add direct outbound + direct := Proxy{ + Type: Direct, + Tag: "DIRECT", + } + // add block outbound + block := Proxy{ + Type: Block, + Tag: "block", + } + // add dns outbound + dns := Proxy{ + Type: DNS, + Tag: "dns-out", + } + proxies = append(proxies, direct, block, dns) + + var rawConfig map[string]any + if err := json.Unmarshal([]byte(DefaultTemplate), &rawConfig); err != nil { + return nil, err + } + + rawConfig["outbounds"] = proxies + route := RouteOptions{ + Final: "手动选择", + Rules: []Rule{ + { + Inbound: []string{ + "tun-in", + "mixed-in", + }, + Action: "sniff", + }, + { + Type: "logical", + Mode: "or", + Rules: []Rule{ + { + Port: []uint16{53}, + }, + { + Protocol: []string{"dns"}, + }, + }, + Action: "hijack-dns", + }, + { + RuleSet: []string{ + "geosite-category-ads-all", + }, + ClashMode: "rule", + Action: "reject", + }, + { + ClashMode: "direct", + Outbound: "DIRECT", + }, + { + ClashMode: "global", + Outbound: "手动选择", + }, + { + IPIsPrivate: true, + Outbound: "DIRECT", + }, + { + RuleSet: []string{ + "geosite-private", + }, + Outbound: "DIRECT", + }, + }, + RuleSet: []RuleSet{ + { + Tag: "geoip-cn", + Type: "remote", + Format: "binary", + URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/cn.srs", + DownloadDetour: "DIRECT", + }, + { + Tag: "geosite-cn", + Type: "remote", + Format: "binary", + URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/cn.srs", + DownloadDetour: "DIRECT", + }, + { + Tag: "geosite-private", + Type: "remote", + Format: "binary", + URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/private.srs", + DownloadDetour: "DIRECT", + }, + { + Tag: "geosite-category-ads-all", + Type: "remote", + Format: "binary", + URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/category-ads-all.srs", + DownloadDetour: "DIRECT", + }, + { + Tag: "geosite-geolocation-!cn", + Type: "remote", + Format: "binary", + URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/geolocation-!cn.srs", + DownloadDetour: "DIRECT", + }, + }, + AutoDetectInterface: true, + } + route.Rules = append(route.Rules, adapterToSingboxRule(adapter.Rules)...) + rawConfig["route"] = route + return json.Marshal(rawConfig) +} + +func buildProxy(data proxy.Proxy, uuid string) *Proxy { + var p *Proxy + var err error + switch data.Protocol { + case VLESS: + p, err = ParseVless(data, uuid) + case Shadowsocks: + p, err = ParseShadowsocks(data, uuid) + case Trojan: + p, err = ParseTrojan(data, uuid) + case VMess: + p, err = ParseVMess(data, uuid) + + case Hysteria2: + p, err = ParseHysteria2(data, uuid) + + case TUIC: + p, err = ParseTUIC(data, uuid) + + default: + logger.Error("Unknown protocol", logger.Field("protocol", data.Protocol), logger.Field("server", data.Name)) + } + if err != nil { + logger.Error("ParseVless", logger.Field("error", err.Error()), logger.Field("server", data.Name), logger.Field("protocol", data.Protocol)) + return nil + } + return p +} diff --git a/pkg/adapter/singbox/default.go b/pkg/adapter/singbox/default.go new file mode 100644 index 0000000..d73b236 --- /dev/null +++ b/pkg/adapter/singbox/default.go @@ -0,0 +1,100 @@ +package singbox + +const DefaultTemplate = ` +{ + "log": { + "level": "info", + "timestamp": true + }, + "experimental": { + "clash_api": { + "external_controller": "127.0.0.1:9090", + "external_ui": "ui", + "secret": "", + "external_ui_download_url": "https://mirror.ghproxy.com/https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip", + "external_ui_download_detour": "direct", + "default_mode": "rule" + }, + "cache_file": { + "enabled": true, + "store_fakeip": false + } + }, + "dns": { + "servers": [ + { + "tag": "dns_proxy", + "address": "tls://8.8.8.8", + "detour": "手动选择" + }, + { + "tag": "dns_direct", + "address": "https://223.5.5.5/dns-query", + "detour": "DIRECT" + } + ], + "rules": [ + { + "outbound": "any", + "server": "dns_direct", + "disable_cache": true + }, + { + "rule_set": "geosite-cn", + "server": "dns_direct" + }, + { + "clash_mode": "direct", + "server": "dns_direct" + }, + { + "clash_mode": "global", + "server": "dns_proxy" + }, + { + "rule_set": "geosite-geolocation-!cn", + "server": "dns_proxy" + } + ], + "final": "dns_direct", + "strategy": "ipv4_only" + }, + "route": { + "rules": [ + { + "action": "sniff" + }, + { + "protocol": "dns", + "action": "hijack-dns" + } + ] + }, + "inbounds": [ + { + "tag": "tun-in", + "type": "tun", + "address": [ + "172.18.0.1/30", + "fdfe:dcba:9876::1/126" + ], + "auto_route": true, + "strict_route": true, + "stack": "system", + "platform": { + "http_proxy": { + "enabled": true, + "server": "127.0.0.1", + "server_port": 7890 + } + } + }, + { + "tag": "mixed-in", + "type": "mixed", + "listen": "127.0.0.1", + "listen_port": 7890 + } + ] +} +` diff --git a/pkg/adapter/singbox/hysteria2.go b/pkg/adapter/singbox/hysteria2.go new file mode 100644 index 0000000..e7b9f55 --- /dev/null +++ b/pkg/adapter/singbox/hysteria2.go @@ -0,0 +1,76 @@ +package singbox + +import ( + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +type Hysteria2Obfs struct { + Type string `json:"type,omitempty"` + Password string `json:"password,omitempty"` +} + +type Hysteria2OutboundOptions struct { + ServerOptions + ServerPorts []string `json:"server_ports,omitempty"` + HopInterval int `json:"hop_interval,omitempty"` + UpMbps int `json:"up_mbps,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` + Obfs *Hysteria2Obfs `json:"obfs,omitempty"` + Password string `json:"password,omitempty"` + Network string `json:"network,omitempty"` + OutboundTLSOptionsContainer + Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` +} + +func ParseHysteria2(data proxy.Proxy, password string) (*Proxy, error) { + hysteria2 := data.Option.(proxy.Hysteria2) + + p := &Proxy{ + Tag: data.Name, + Type: Hysteria2, + Hysteria2Options: &Hysteria2OutboundOptions{ + ServerOptions: ServerOptions{ + Tag: data.Name, + Type: Hysteria2, + Server: data.Server, + }, + Password: password, + }, + } + + var ports []string + + if hysteria2.HopPorts != "" { + ps := strings.Split(hysteria2.HopPorts, ",") + for _, port := range ps { + // 舍弃单个端口,只保留端口范围 + if len(strings.Split(port, "-")) > 1 { + tmp := strings.Split(port, "-") + ports = append(ports, strings.Join(tmp, ":")) + } + } + + } + if len(ports) > 0 { + p.Hysteria2Options.ServerPorts = ports + p.Hysteria2Options.HopInterval = hysteria2.HopInterval + } else { + p.Hysteria2Options.ServerPort = data.Port + } + + if hysteria2.ObfsPassword != "" { + p.Hysteria2Options.Obfs = &Hysteria2Obfs{ + Type: "salamander", + Password: hysteria2.ObfsPassword, + } + } + var tls *OutboundTLSOptions + if hysteria2.SecurityConfig.SNI != "" { + tls = NewOutboundTLSOptions("tls", hysteria2.SecurityConfig) + } + p.Hysteria2Options.TLS = tls + return p, nil +} diff --git a/pkg/adapter/singbox/multiplex.go b/pkg/adapter/singbox/multiplex.go new file mode 100644 index 0000000..7188f95 --- /dev/null +++ b/pkg/adapter/singbox/multiplex.go @@ -0,0 +1,17 @@ +package singbox + +type OutboundMultiplexOptions struct { + Enabled bool `json:"enabled,omitempty"` + Protocol string `json:"protocol,omitempty"` + MaxConnections int `json:"max_connections,omitempty"` + MinStreams int `json:"min_streams,omitempty"` + MaxStreams int `json:"max_streams,omitempty"` + Padding bool `json:"padding,omitempty"` + Brutal *BrutalOptions `json:"brutal,omitempty"` +} + +type BrutalOptions struct { + Enabled bool `json:"enabled,omitempty"` + UpMbps int `json:"up_mbps,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` +} diff --git a/pkg/adapter/singbox/rule.go b/pkg/adapter/singbox/rule.go new file mode 100644 index 0000000..af00e56 --- /dev/null +++ b/pkg/adapter/singbox/rule.go @@ -0,0 +1,130 @@ +package singbox + +import ( + "strconv" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/rules" +) + +type Rule struct { + Outbound string `json:"outbound,omitempty"` + ClashMode string `json:"clash_mode,omitempty"` + RuleSet []string `json:"rule_set,omitempty"` + Domain []string `json:"domain,omitempty"` + DomainSuffix []string `json:"domain_suffix,omitempty"` + DomainKeyword []string `json:"domain_keyword,omitempty"` + DomainRegex []string `json:"domain_regex,omitempty"` + GeoIP []string `json:"geoip,omitempty"` + IPCIDR []string `json:"ip_cidr,omitempty"` + IPIsPrivate bool `json:"ip_is_private,omitempty"` + SourceIPCIDR []string `json:"source_ip_cidr,omitempty"` + ProcessName []string `json:"process_name,omitempty"` + ProcessPath []string `json:"process_path,omitempty"` + SourcePort []uint16 `json:"source_port,omitempty"` + Protocol []string `json:"protocol,omitempty"` + Port []uint16 `json:"port,omitempty"` + Action string `json:"action,omitempty"` + Inbound []string `json:"inbound,omitempty"` + Rules []Rule `json:"rules,omitempty"` + Type string `json:"type,omitempty"` + Mode string `json:"mode,omitempty"` +} + +type RuleSet struct { + Tag string `json:"tag,omitempty"` + Type string `json:"type,omitempty"` + Format string `json:"format,omitempty"` + URL string `json:"url,omitempty"` + DownloadDetour string `json:"download_detour,omitempty"` +} + +func adapterToSingboxRule(texts []string) []Rule { + var rulesList []Rule + for _, rule := range texts { + r := rules.NewRule(rule, "") + if r == nil { + continue + } + rulesList = addRuleToItem(rulesList, r.Target, *r) + } + return rulesList +} + +func addRuleToItem(group []Rule, outbound string, rule rules.Rule) []Rule { + for i := range group { + if group[i].Outbound == outbound { + switch rules.ParseRuleType(rule.Type) { + case rules.Domain: + group[i].Domain = append(group[i].Domain, rule.Payload) + return group + case rules.DomainSuffix: + group[i].DomainSuffix = append(group[i].DomainSuffix, rule.Payload) + return group + case rules.DomainKeyword: + group[i].DomainKeyword = append(group[i].DomainKeyword, rule.Payload) + return group + case rules.IPCIDR: + group[i].IPCIDR = append(group[i].IPCIDR, rule.Payload) + return group + case rules.SrcIPCIDR: + group[i].SourceIPCIDR = append(group[i].SourceIPCIDR, rule.Payload) + return group + case rules.SrcPort: + port, err := strconv.ParseUint(rule.Payload, 10, 16) + if err != nil { + logger.Errorf("[adapterToSingboxRule] failed to parse port %s to uint16", rule.Payload) + return group + } + group[i].SourcePort = append(group[i].SourcePort, uint16(port)) + return group + case rules.GEOIP: + group[i].GeoIP = append(group[i].GeoIP, rule.Payload) + return group + case rules.Process: + group[i].ProcessName = append(group[i].ProcessName, rule.Payload) + return group + case rules.ProcessPath: + group[i].ProcessPath = append(group[i].ProcessPath, rule.Payload) + return group + default: + logger.Errorf("[adapterToSingboxRule] unknown rule type %s", rule.Type) + return group + } + } + } + newRule := Rule{ + Outbound: outbound, + } + + switch rules.ParseRuleType(rule.Type) { + case rules.Domain: + newRule.Domain = []string{rule.Payload} + case rules.DomainSuffix: + newRule.DomainSuffix = []string{rule.Payload} + case rules.DomainKeyword: + newRule.DomainKeyword = []string{rule.Payload} + case rules.IPCIDR: + newRule.IPCIDR = []string{rule.Payload} + case rules.SrcIPCIDR: + newRule.SourceIPCIDR = []string{rule.Payload} + case rules.SrcPort: + port, err := strconv.ParseUint(rule.Payload, 10, 16) + if err != nil { + logger.Errorf("[adapterToSingboxRule] failed to parse port %s to uint16", rule.Payload) + return group + } + newRule.SourcePort = []uint16{uint16(port)} + case rules.GEOIP: + newRule.GeoIP = []string{rule.Payload} + case rules.Process: + newRule.ProcessName = []string{rule.Payload} + case rules.ProcessPath: + newRule.ProcessPath = []string{rule.Payload} + default: + logger.Errorf("[adapterToSingboxRule] unknown rule type %s", rule.Type) + return group + } + group = append(group, newRule) + return group +} diff --git a/pkg/adapter/singbox/rule_test.go b/pkg/adapter/singbox/rule_test.go new file mode 100644 index 0000000..95f8ce3 --- /dev/null +++ b/pkg/adapter/singbox/rule_test.go @@ -0,0 +1,15 @@ +package singbox + +import ( + "fmt" + "testing" +) + +func TestAdapterToSingboxRule(t *testing.T) { + rules := []string{ + "DOMAIN,example.com,DIRECT", + "DOMAIN-SUFFIX,google.com,智能线路", + } + result := adapterToSingboxRule(rules) + fmt.Printf("TestAdapterToSingboxRule: result: %+v\n", result) +} diff --git a/pkg/adapter/singbox/shadowsocks.go b/pkg/adapter/singbox/shadowsocks.go new file mode 100644 index 0000000..e0d59b0 --- /dev/null +++ b/pkg/adapter/singbox/shadowsocks.go @@ -0,0 +1,34 @@ +package singbox + +import ( + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +type ShadowsocksOptions struct { + ServerOptions + Method string `json:"method,omitempty"` + Password string `json:"password,omitempty"` + Plugin string `json:"plugin,omitempty"` + PluginOptions string `json:"plugin_opts,omitempty"` + Network string `json:"network,omitempty"` +} + +func ParseShadowsocks(data proxy.Proxy, uuid string) (*Proxy, error) { + config := data.Option.(proxy.Shadowsocks) + p := &Proxy{ + Tag: data.Name, + Type: Shadowsocks, + ShadowsocksOptions: &ShadowsocksOptions{ + ServerOptions: ServerOptions{ + Tag: data.Name, + Type: Shadowsocks, + Server: data.Server, + ServerPort: data.Port, + }, + Method: config.Method, + Password: uuid, + Network: "tcp", + }, + } + return p, nil +} diff --git a/pkg/adapter/singbox/singbox.go b/pkg/adapter/singbox/singbox.go new file mode 100644 index 0000000..0cb40f5 --- /dev/null +++ b/pkg/adapter/singbox/singbox.go @@ -0,0 +1,98 @@ +package singbox + +import ( + "encoding/json" + "fmt" +) + +const ( + Trojan = "trojan" + VLESS = "vless" + VMess = "vmess" + TUIC = "tuic" + Hysteria2 = "hysteria2" + Shadowsocks = "shadowsocks" + Selector = "selector" + URLTest = "urltest" + Direct = "direct" + Block = "block" + DNS = "dns" +) + +type Proxy struct { + Tag string `json:"tag,omitempty"` + Type string `json:"type"` + ShadowsocksOptions *ShadowsocksOptions `json:"-"` + TUICOptions *TUICOutboundOptions `json:"-"` + TrojanOptions *TrojanOutboundOptions `json:"-"` + VLESSOptions *VLESSOutboundOptions `json:"-"` + VMessOptions *VMessOutboundOptions `json:"-"` + Hysteria2Options *Hysteria2OutboundOptions `json:"-"` + SelectorOptions *SelectorOutboundOptions `json:"-"` + URLTestOptions *URLTestOutboundOptions `json:"-"` +} + +type ServerOptions struct { + Tag string `json:"tag"` + Type string `json:"type"` + Server string `json:"server"` + ServerPort int `json:"server_port,omitempty"` +} +type OutboundOptions struct { + Tag string `json:"tag"` + Type string `json:"type"` +} +type SelectorOutboundOptions struct { + OutboundOptions + Outbounds []string `json:"outbounds"` + Default string `json:"default,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} + +type URLTestOutboundOptions struct { + OutboundOptions + Outbounds []string `json:"outbounds"` + URL string `json:"url,omitempty"` + Interval Duration `json:"interval,omitempty"` + Tolerance uint16 `json:"tolerance,omitempty"` + IdleTimeout Duration `json:"idle_timeout,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} + +type RouteOptions struct { + Rules []Rule `json:"rules,omitempty"` + Final string `json:"final,omitempty"` + RuleSet []RuleSet `json:"rule_set,omitempty"` + AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` +} + +func (p Proxy) MarshalJSON() ([]byte, error) { + type Alias Proxy + aux := struct { + Alias + }{ + Alias: (Alias)(p), + } + switch p.Type { + case Shadowsocks: + return json.Marshal(p.ShadowsocksOptions) + case TUIC: + return json.Marshal(p.TUICOptions) + case Trojan: + return json.Marshal(p.TrojanOptions) + case VLESS: + return json.Marshal(p.VLESSOptions) + case VMess: + return json.Marshal(p.VMessOptions) + case Hysteria2: + return json.Marshal(p.Hysteria2Options) + case Selector: + return json.Marshal(p.SelectorOptions) + case URLTest: + return json.Marshal(p.URLTestOptions) + case Direct, Block, DNS: + return json.Marshal(aux.Alias) + default: + return nil, fmt.Errorf("[sing-box] MarshalJSON unknown type: %s", p.Type) + } +} diff --git a/pkg/adapter/singbox/singbox_test.go b/pkg/adapter/singbox/singbox_test.go new file mode 100644 index 0000000..0d9c335 --- /dev/null +++ b/pkg/adapter/singbox/singbox_test.go @@ -0,0 +1,80 @@ +package singbox + +import ( + "testing" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" + + "github.com/stretchr/testify/assert" +) + +func createSS() proxy.Proxy { + c := proxy.Shadowsocks{ + Method: "aes-256-gcm", + Port: 10301, + ServerKey: "", + } + return proxy.Proxy{ + Name: "Shadowsocks", + Server: "127.0.0.1", + Port: 10301, + Protocol: "shadowsocks", + Option: c, + } +} + +func createVLESS() proxy.Proxy { + c := proxy.Vless{ + Port: 10301, + Flow: "xtls-rprx-direct", + Transport: "websocket", + TransportConfig: proxy.TransportConfig{ + Path: "/ws", + Host: "baidu.com", + }, + Security: "tls", + SecurityConfig: proxy.SecurityConfig{ + SNI: "baidu.com", + Fingerprint: "chrome", + AllowInsecure: true, + }, + } + s := proxy.Proxy{ + Name: "VLESS", + Server: "test.xxx.com", + Port: 10301, + Protocol: "vless", + Option: c, + } + return s +} + +func TestSingboxShadowsocks(t *testing.T) { + s := createSS() + p, err := ParseShadowsocks(s, "uuid") + if err != nil { + t.Fatal(err) + } + data, err := p.MarshalJSON() + if err != nil { + t.Fatal(err) + } + assert.NotEqual(t, 0, len(data)) + + // Output: + // proxy: proxy: {"tag":"Shadowsocks","type":"shadowsocks","server":"127.0.0.1","server_port":10301,"method":"aes-256-gcm","password":"uuid","network":"tcp"} + +} + +func TestSingboxVless(t *testing.T) { + s := createVLESS() + p, err := ParseVless(s, "uuid") + if err != nil { + t.Fatal(err) + } + data, err := p.MarshalJSON() + if err != nil { + t.Fatal(err) + } + assert.NotEqual(t, 0, len(data)) +} diff --git a/pkg/adapter/singbox/tls.go b/pkg/adapter/singbox/tls.go new file mode 100644 index 0000000..fa7c1f9 --- /dev/null +++ b/pkg/adapter/singbox/tls.go @@ -0,0 +1,87 @@ +package singbox + +import ( + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +type OutboundTLSOptions struct { + Enabled bool `json:"enabled,omitempty"` + DisableSNI bool `json:"disable_sni,omitempty"` + ServerName string `json:"server_name,omitempty"` + Insecure bool `json:"insecure,omitempty"` + ALPN Listable[string] `json:"alpn,omitempty"` + MinVersion string `json:"min_version,omitempty"` + MaxVersion string `json:"max_version,omitempty"` + CipherSuites Listable[string] `json:"cipher_suites,omitempty"` + Certificate Listable[string] `json:"certificate,omitempty"` + CertificatePath string `json:"certificate_path,omitempty"` + ECH *OutboundECHOptions `json:"ech,omitempty"` + UTLS *OutboundUTLSOptions `json:"utls,omitempty"` + Reality *OutboundRealityOptions `json:"reality,omitempty"` +} + +func NewOutboundTLSOptions(security string, cfg proxy.SecurityConfig) *OutboundTLSOptions { + var tls = &OutboundTLSOptions{} + switch security { + case "none": + return nil + case "tls": + tls.Enabled = true + if cfg.SNI != "" { + tls.ServerName = cfg.SNI + } else { + tls.DisableSNI = true + } + tls.Insecure = cfg.AllowInsecure + if cfg.Fingerprint != "" { + tls.UTLS = &OutboundUTLSOptions{ + Enabled: true, + Fingerprint: cfg.Fingerprint, + } + } + case "reality": + tls.Enabled = true + if cfg.SNI != "" { + tls.ServerName = cfg.SNI + } else { + tls.DisableSNI = true + } + tls.Insecure = cfg.AllowInsecure + if cfg.Fingerprint != "" { + tls.UTLS = &OutboundUTLSOptions{ + Enabled: true, + Fingerprint: cfg.Fingerprint, + } + } + tls.Reality = &OutboundRealityOptions{ + Enabled: true, + PublicKey: cfg.RealityPublicKey, + ShortID: cfg.RealityShortId, + } + } + return tls +} + +type OutboundECHOptions struct { + Enabled bool `json:"enabled,omitempty"` + PQSignatureSchemesEnabled bool `json:"pq_signature_schemes_enabled,omitempty"` + DynamicRecordSizingDisabled bool `json:"dynamic_record_sizing_disabled,omitempty"` + Config Listable[string] `json:"config,omitempty"` + ConfigPath string `json:"config_path,omitempty"` +} + +type OutboundRealityOptions struct { + Enabled bool `json:"enabled,omitempty"` + PublicKey string `json:"public_key,omitempty"` + ShortID string `json:"short_id,omitempty"` +} + +type OutboundUTLSOptions struct { + Enabled bool `json:"enabled,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` +} +type Listable[T any] []T + +type OutboundTLSOptionsContainer struct { + TLS *OutboundTLSOptions `json:"tls,omitempty"` +} diff --git a/pkg/adapter/singbox/tool.go b/pkg/adapter/singbox/tool.go new file mode 100644 index 0000000..535dbe8 --- /dev/null +++ b/pkg/adapter/singbox/tool.go @@ -0,0 +1,11 @@ +package singbox + +import "encoding/json" + +func mergeOptions(target map[string]any, options any) error { + optionsJSON, err := json.Marshal(options) + if err != nil { + return err + } + return json.Unmarshal(optionsJSON, &target) +} diff --git a/pkg/adapter/singbox/trojan.go b/pkg/adapter/singbox/trojan.go new file mode 100644 index 0000000..9233ff1 --- /dev/null +++ b/pkg/adapter/singbox/trojan.go @@ -0,0 +1,39 @@ +package singbox + +import ( + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +type TrojanOutboundOptions struct { + ServerOptions + Password string `json:"password"` + Network string `json:"network,omitempty"` + OutboundTLSOptionsContainer + Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` +} + +func ParseTrojan(data proxy.Proxy, uuid string) (*Proxy, error) { + trojan := data.Option.(proxy.Trojan) + p := &Proxy{ + Tag: data.Name, + Type: Trojan, + TrojanOptions: &TrojanOutboundOptions{ + ServerOptions: ServerOptions{ + Tag: data.Name, + Type: Trojan, + Server: data.Server, + ServerPort: data.Port, + }, + Password: uuid, + }, + } + // Transport options + transport := NewV2RayTransportOptions(trojan.Transport, trojan.TransportConfig) + + p.TrojanOptions.Transport = transport + // Security options + p.TrojanOptions.TLS = NewOutboundTLSOptions(trojan.Security, trojan.SecurityConfig) + return p, nil + +} diff --git a/pkg/adapter/singbox/tuic.go b/pkg/adapter/singbox/tuic.go new file mode 100644 index 0000000..9abdab2 --- /dev/null +++ b/pkg/adapter/singbox/tuic.go @@ -0,0 +1,40 @@ +package singbox + +import ( + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +type TUICOutboundOptions struct { + ServerOptions + UUID string `json:"uuid,omitempty"` + Password string `json:"password,omitempty"` + CongestionControl string `json:"congestion_control,omitempty"` + UDPRelayMode string `json:"udp_relay_mode,omitempty"` + UDPOverStream bool `json:"udp_over_stream,omitempty"` + ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` + Heartbeat string `json:"heartbeat,omitempty"` + Network string `json:"network,omitempty"` + OutboundTLSOptionsContainer +} + +func ParseTUIC(data proxy.Proxy, uuid string) (*Proxy, error) { + tuic := data.Option.(proxy.Tuic) + p := &Proxy{ + Tag: data.Name, + Type: TUIC, + TUICOptions: &TUICOutboundOptions{ + ServerOptions: ServerOptions{ + Tag: data.Name, + Type: TUIC, + Server: data.Server, + ServerPort: data.Port, + }, + UUID: uuid, + Password: uuid, + CongestionControl: "bbr", + }, + } + // Security options + p.TUICOptions.TLS = NewOutboundTLSOptions("tls", tuic.SecurityConfig) + return p, nil +} diff --git a/pkg/adapter/singbox/v2rayTransport.go b/pkg/adapter/singbox/v2rayTransport.go new file mode 100644 index 0000000..9ba3f10 --- /dev/null +++ b/pkg/adapter/singbox/v2rayTransport.go @@ -0,0 +1,114 @@ +package singbox + +import ( + "encoding/json" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +type V2RayTransportOptions struct { + Type string `json:"type"` + HTTPOptions V2RayHTTPOptions `json:"-"` + WebsocketOptions V2RayWebsocketOptions `json:"-"` + QUICOptions V2RayQUICOptions `json:"-"` + GRPCOptions V2RayGRPCOptions `json:"-"` + HTTPUpgradeOptions V2RayHTTPUpgradeOptions `json:"-"` +} + +func (v V2RayTransportOptions) MarshalJSON() ([]byte, error) { + var v2rayTransportOptions any + data := map[string]any{ + "type": v.Type, + } + switch v.Type { + case "http": + v2rayTransportOptions = v.HTTPOptions + case "ws": + v2rayTransportOptions = v.WebsocketOptions + case "quic": + v2rayTransportOptions = v.QUICOptions + case "grpc": + v2rayTransportOptions = v.GRPCOptions + case "httpupgrade": + v2rayTransportOptions = v.HTTPUpgradeOptions + } + if err := mergeOptions(data, v2rayTransportOptions); err != nil { + return nil, err + } + return json.Marshal(data) +} + +func NewV2RayTransportOptions(network string, transport proxy.TransportConfig) *V2RayTransportOptions { + var t *V2RayTransportOptions = nil + switch network { + case "websocket": + t = &V2RayTransportOptions{ + Type: "ws", + WebsocketOptions: V2RayWebsocketOptions{ + Path: transport.Path, + Headers: map[string]Listable[string]{ + "Host": []string{transport.Host}, + }, + MaxEarlyData: 2048, + EarlyDataHeaderName: "Sec-WebSocket-Protocol", + }, + } + case "httpupgrade": + t = &V2RayTransportOptions{ + Type: "httpupgrade", + HTTPOptions: V2RayHTTPOptions{ + Path: transport.Path, + Host: []string{transport.Host}, + Headers: map[string]Listable[string]{ + "Host": []string{transport.Host}, + }, + }, + } + + case "grpc": + t = &V2RayTransportOptions{ + Type: "grpc", + GRPCOptions: V2RayGRPCOptions{ + ServiceName: transport.ServiceName, + }, + } + } + return t +} + +type V2RayHTTPOptions struct { + Host Listable[string] `json:"host,omitempty"` + Path string `json:"path,omitempty"` + Method string `json:"method,omitempty"` + Headers HTTPHeader `json:"headers,omitempty"` + IdleTimeout Duration `json:"idle_timeout,omitempty"` + PingTimeout Duration `json:"ping_timeout,omitempty"` +} + +type V2RayWebsocketOptions struct { + Path string `json:"path,omitempty"` + Headers HTTPHeader `json:"headers,omitempty"` + MaxEarlyData uint32 `json:"max_early_data,omitempty"` + EarlyDataHeaderName string `json:"early_data_header_name,omitempty"` +} + +type V2RayQUICOptions struct{} + +type V2RayGRPCOptions struct { + ServiceName string `json:"service_name,omitempty"` + IdleTimeout string `json:"idle_timeout,omitempty"` + PingTimeout string `json:"ping_timeout,omitempty"` + PermitWithoutStream bool `json:"permit_without_stream,omitempty"` + ForceLite bool `json:"-"` // for test +} + +type V2RayHTTPUpgradeOptions struct { + Host string `json:"host,omitempty"` + Path string `json:"path,omitempty"` + Headers HTTPHeader `json:"headers,omitempty"` +} + +type HTTPHeader map[string]Listable[string] + +type Duration time.Duration diff --git a/pkg/adapter/singbox/vless.go b/pkg/adapter/singbox/vless.go new file mode 100644 index 0000000..e1038ed --- /dev/null +++ b/pkg/adapter/singbox/vless.go @@ -0,0 +1,44 @@ +package singbox + +import ( + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +type VLESSOutboundOptions struct { + ServerOptions + OutboundTLSOptionsContainer + UUID string `json:"uuid"` + Flow string `json:"flow,omitempty"` + Network string `json:"network,omitempty"` + Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` + PacketEncoding *string `json:"packet_encoding,omitempty"` +} + +func ParseVless(data proxy.Proxy, uuid string) (*Proxy, error) { + vless := data.Option.(proxy.Vless) + packetEncoding := "xudp" + p := &Proxy{ + Tag: data.Name, + Type: VLESS, + VLESSOptions: &VLESSOutboundOptions{ + ServerOptions: ServerOptions{ + Tag: data.Name, + Type: VLESS, + Server: data.Server, + ServerPort: data.Port, + }, + UUID: uuid, + Flow: vless.Flow, + PacketEncoding: &packetEncoding, + }, + } + // Transport options + transport := NewV2RayTransportOptions(vless.Transport, vless.TransportConfig) + p.VLESSOptions.Transport = transport + + // Security options + p.VLESSOptions.TLS = NewOutboundTLSOptions(vless.Security, vless.SecurityConfig) + + return p, nil +} diff --git a/pkg/adapter/singbox/vmess.go b/pkg/adapter/singbox/vmess.go new file mode 100644 index 0000000..daf062f --- /dev/null +++ b/pkg/adapter/singbox/vmess.go @@ -0,0 +1,43 @@ +package singbox + +import ( + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +type VMessOutboundOptions struct { + ServerOptions + UUID string `json:"uuid"` + Security string `json:"security"` + AlterId int `json:"alter_id,omitempty"` + GlobalPadding bool `json:"global_padding,omitempty"` + AuthenticatedLength bool `json:"authenticated_length,omitempty"` + Network string `json:"network,omitempty"` + PacketEncoding string `json:"packet_encoding,omitempty"` + Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` + OutboundTLSOptionsContainer +} + +func ParseVMess(data proxy.Proxy, uuid string) (*Proxy, error) { + vmess := data.Option.(proxy.Vmess) + p := &Proxy{ + Type: VMess, + VMessOptions: &VMessOutboundOptions{ + ServerOptions: ServerOptions{ + Tag: data.Name, + Type: VMess, + Server: data.Server, + ServerPort: data.Port, + }, + UUID: uuid, + Security: "auto", + AlterId: 0, + }, + } + // Transport options + p.VMessOptions.Transport = NewV2RayTransportOptions(vmess.Transport, vmess.TransportConfig) + // Security options + p.VMessOptions.TLS = NewOutboundTLSOptions(vmess.Security, vmess.SecurityConfig) + + return p, nil +} diff --git a/pkg/adapter/surfboard/build.go b/pkg/adapter/surfboard/build.go new file mode 100644 index 0000000..879a602 --- /dev/null +++ b/pkg/adapter/surfboard/build.go @@ -0,0 +1,111 @@ +package surfboard + +import ( + "bytes" + "embed" + "fmt" + "net/url" + "strings" + "text/template" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/traffic" +) + +//go:embed *.tpl +var configFiles embed.FS +var shadowsocksSupportMethod = []string{"aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305"} + +func BuildSurfboard(servers proxy.Adapter, siteName string, user UserInfo) []byte { + var proxies, proxyGroup string + for _, node := range servers.Proxies { + if uri := buildProxy(node, user.UUID); uri != "" { + proxies += uri + } + } + + for _, group := range servers.Group { + if group.Type == proxy.GroupTypeSelect { + proxyGroup += fmt.Sprintf("%s = select, %s", group.Name, strings.Join(group.Proxies, ", ")) + "\r\n" + } else if group.Type == proxy.GroupTypeURLTest { + proxyGroup += fmt.Sprintf("%s = url-test, %s, url=%s, interval=%d", group.Name, strings.Join(group.Proxies, ", "), group.URL, group.Interval) + "\r\n" + } else if group.Type == proxy.GroupTypeFallback { + proxyGroup += fmt.Sprintf("%s = fallback, %s, url=%s, interval=%d", group.Name, strings.Join(group.Proxies, ", "), group.URL, group.Interval) + "\r\n" + } else { + logger.Errorf("[BuildSurfboard] unknown group type: %s", group.Type) + } + } + + var rules string + for _, rule := range servers.Rules { + if rule == "" { + continue + } + rules += rule + "\r\n" + } + + //final rule + rules += "# 最终规则" + "\r\n" + "FINAL, 手动选择" + + file, err := configFiles.ReadFile("default.tpl") + if err != nil { + logger.Errorf("read default surfboard config error: %v", err.Error()) + return nil + } + // replace template + tpl, err := template.New("default").Parse(string(file)) + if err != nil { + logger.Errorf("read default surfboard config error: %v", err.Error()) + return nil + } + var buf bytes.Buffer + + var expiredAt string + if user.ExpiredDate.Before(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)) { + expiredAt = "长期有效" + } else { + expiredAt = user.ExpiredDate.Format("2006-01-02 15:04:05") + } + // convert traffic + upload := traffic.AutoConvert(user.Upload, false) + download := traffic.AutoConvert(user.Download, false) + total := traffic.AutoConvert(user.TotalTraffic, false) + unusedTraffic := traffic.AutoConvert(user.TotalTraffic-user.Upload-user.Download, false) + // query Host + urlParse, err := url.Parse(user.SubscribeURL) + if err != nil { + return nil + } + if err := tpl.Execute(&buf, map[string]interface{}{ + "Proxies": proxies, + "ProxyGroup": proxyGroup, + "SubscribeURL": user.SubscribeURL, + "SubscribeInfo": fmt.Sprintf("title=%s订阅信息, content=上传流量:%s\\n下载流量:%s\\n剩余流量: %s\\n套餐流量:%s\\n到期时间:%s", siteName, upload, download, unusedTraffic, total, expiredAt), + "SubscribeDomain": urlParse.Host, + "Rules": rules, + }); err != nil { + logger.Errorf("build surfboard config error: %v", err.Error()) + return nil + } + return buf.Bytes() +} + +func buildProxy(data proxy.Proxy, uuid string) string { + var p string + switch data.Protocol { + case "vmess": + p = buildVMess(data, uuid) + case "shadowsocks": + if !tool.Contains(shadowsocksSupportMethod, data.Option.(proxy.Shadowsocks).Method) { + return "" + } + p = buildShadowsocks(data, uuid) + case "trojan": + p = buildTrojan(data, uuid) + } + return p +} diff --git a/pkg/adapter/surfboard/build_test.go b/pkg/adapter/surfboard/build_test.go new file mode 100644 index 0000000..f3773b1 --- /dev/null +++ b/pkg/adapter/surfboard/build_test.go @@ -0,0 +1,24 @@ +package surfboard + +import ( + "testing" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" + + "github.com/perfect-panel/ppanel-server/pkg/uuidx" +) + +func TestBuildSurfboard(t *testing.T) { + siteName := "test" + user := UserInfo{ + UUID: uuidx.NewUUID().String(), + Upload: 0, + Download: 0, + TotalTraffic: 0, + ExpiredDate: time.Now().AddDate(0, 1, 1), + SubscribeURL: "https://test.com", + } + conf := BuildSurfboard(proxy.Adapter{}, siteName, user) + t.Log(string(conf)) +} diff --git a/pkg/adapter/surfboard/default.tpl b/pkg/adapter/surfboard/default.tpl new file mode 100644 index 0000000..e30aac0 --- /dev/null +++ b/pkg/adapter/surfboard/default.tpl @@ -0,0 +1,29 @@ +#!MANAGED-CONFIG {{ .SubscribeURL }} interval=43200 strict=true + +[General] +loglevel = notify +ipv6 = false +skip-proxy = localhost, *.local, injections.adguard.org, local.adguard.org, 0.0.0.0/8, 10.0.0.0/8, 17.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.168.0.0/16, 192.88.99.0/24, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 240.0.0.0/4, 255.255.255.255/32 +tls-provider = default +show-error-page-for-reject = true +dns-server = 223.6.6.6, 119.29.29.29, 119.28.28.28 +test-timeout = 5 +internet-test-url = http://bing.com +proxy-test-url = http://bing.com + +[Panel] +SubscribeInfo = {{ .SubscribeInfo }}, style=info + +# Surfboard 配置文档:https://manual.getsurfboard.com/ + +[Proxy] +# 代理列表 +{{ .Proxies }} + +[Proxy Group] +# 代理组列表 +{{ .ProxyGroup }} + +[Rule] +# 规则列表 +{{ .Rules }} diff --git a/pkg/adapter/surfboard/model.go b/pkg/adapter/surfboard/model.go new file mode 100644 index 0000000..29d53ba --- /dev/null +++ b/pkg/adapter/surfboard/model.go @@ -0,0 +1,12 @@ +package surfboard + +import "time" + +type UserInfo struct { + UUID string + Upload int64 + Download int64 + TotalTraffic int64 + ExpiredDate time.Time + SubscribeURL string +} diff --git a/pkg/adapter/surfboard/shadowsocks.go b/pkg/adapter/surfboard/shadowsocks.go new file mode 100644 index 0000000..a8d03b4 --- /dev/null +++ b/pkg/adapter/surfboard/shadowsocks.go @@ -0,0 +1,24 @@ +package surfboard + +import ( + "fmt" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func buildShadowsocks(data proxy.Proxy, uuid string) string { + ss, ok := data.Option.(proxy.Shadowsocks) + if !ok { + return "" + } + addr := fmt.Sprintf("%s=ss, %s, %d", data.Name, data.Server, data.Port) + config := []string{ + addr, + fmt.Sprintf("encrypt-method=%s", ss.Method), + fmt.Sprintf("password=%s", uuid), + "tfo=true", + "udp-relay=true", + } + return strings.Join(config, ",") + "\r\n" +} diff --git a/pkg/adapter/surfboard/shadowsocks_test.go b/pkg/adapter/surfboard/shadowsocks_test.go new file mode 100644 index 0000000..a07e2b6 --- /dev/null +++ b/pkg/adapter/surfboard/shadowsocks_test.go @@ -0,0 +1,28 @@ +package surfboard + +import ( + "testing" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func createSS() proxy.Proxy { + return proxy.Proxy{ + Name: "Shadowsocks", + Server: "test.xxxx.com", + Port: 10301, + Protocol: "shadowsocks", + Option: proxy.Shadowsocks{ + Port: 10301, + Method: "aes-256-gcm", + ServerKey: "123456", + }, + } +} + +func TestShadowsocks(t *testing.T) { + node := createSS() + uuid := "123456" + shadowsocks := buildShadowsocks(node, uuid) + t.Log(shadowsocks) +} diff --git a/pkg/adapter/surfboard/trojan.go b/pkg/adapter/surfboard/trojan.go new file mode 100644 index 0000000..86884b3 --- /dev/null +++ b/pkg/adapter/surfboard/trojan.go @@ -0,0 +1,41 @@ +package surfboard + +import ( + "strconv" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func buildTrojan(data proxy.Proxy, uuid string) string { + // $config = [ + // "{$server['name']}=trojan", + // "{$server['host']}", + // "{$server['port']}", + // "password={$password}", + // $protocol_settings['server_name'] ? "sni={$protocol_settings['server_name']}" : "", + // 'tfo=true', + // 'udp-relay=true' + //]; + trojan, ok := data.Option.(proxy.Trojan) + if !ok { + return "" + } + config := []string{ + data.Name + "=trojan", + data.Server, + strconv.Itoa(data.Port), + "password=" + uuid, + "tfo=true", + "udp-relay=true", + } + if trojan.SecurityConfig.SNI != "" { + config = append(config, "sni="+trojan.SecurityConfig.SNI) + } + if trojan.SecurityConfig.AllowInsecure { + config = append(config, "skip-cert-verify=true") + } else { + config = append(config, "skip-cert-verify=false") + } + return strings.Join(config, ",") + "\r\n" +} diff --git a/pkg/adapter/surfboard/trojan_test.go b/pkg/adapter/surfboard/trojan_test.go new file mode 100644 index 0000000..b1938d9 --- /dev/null +++ b/pkg/adapter/surfboard/trojan_test.go @@ -0,0 +1,36 @@ +package surfboard + +import ( + "testing" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func createTrojan() proxy.Proxy { + + return proxy.Proxy{ + Name: "Trojan", + Server: "test.xxxx.com", + Port: 13002, + Protocol: "trojan", + Option: proxy.Trojan{ + Port: 13002, + Transport: "websocket", + TransportConfig: proxy.TransportConfig{ + Path: "/ws", + Host: "baidu.com", + }, + SecurityConfig: proxy.SecurityConfig{ + SNI: "baidu.com", + AllowInsecure: true, + }, + }, + } +} + +func TestTrojan(t *testing.T) { + node := createTrojan() + uuid := "123456" + trojan := buildTrojan(node, uuid) + t.Log(trojan) +} diff --git a/pkg/adapter/surfboard/vmess.go b/pkg/adapter/surfboard/vmess.go new file mode 100644 index 0000000..0d8f57a --- /dev/null +++ b/pkg/adapter/surfboard/vmess.go @@ -0,0 +1,45 @@ +package surfboard + +import ( + "fmt" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func buildVMess(data proxy.Proxy, uuid string) string { + vmess, ok := data.Option.(proxy.Vmess) + if !ok { + return "" + } + addr := fmt.Sprintf("%s=vmess, %s, %d", data.Name, data.Server, data.Port) + uriConfig := []string{ + addr, + fmt.Sprintf("username=%s", uuid), + "vmess-aead=true", + "tfo=true", + "udp-relay=true", + } + if vmess.Security == "tls" { + uriConfig = append(uriConfig, "tls=true") + if vmess.SecurityConfig.AllowInsecure { + uriConfig = append(uriConfig, "skip-cert-verify=true") + } else { + uriConfig = append(uriConfig, "skip-cert-verify=false") + } + if vmess.SecurityConfig.SNI != "" { + uriConfig = append(uriConfig, fmt.Sprintf("sni=%s", vmess.SecurityConfig.SNI)) + } + } + if vmess.Transport == "websocket" { + uriConfig = append(uriConfig, "ws=true") + if vmess.TransportConfig.Path != "" { + uriConfig = append(uriConfig, fmt.Sprintf("ws-path=%s", vmess.TransportConfig.Path)) + } + if vmess.TransportConfig.Host != "" { + uriConfig = append(uriConfig, fmt.Sprintf("ws-headers=Host:%s", vmess.TransportConfig.Host)) + } + } + + return strings.Join(uriConfig, ",") + "\r\n" +} diff --git a/pkg/adapter/surfboard/vmess_test.go b/pkg/adapter/surfboard/vmess_test.go new file mode 100644 index 0000000..4ccb912 --- /dev/null +++ b/pkg/adapter/surfboard/vmess_test.go @@ -0,0 +1,33 @@ +package surfboard + +import ( + "testing" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func createVMess() proxy.Proxy { + + return proxy.Proxy{ + Name: "Vmess", + Server: "test.xxxx.com", + Port: 13002, + Protocol: "vmess", + Option: proxy.Vmess{ + Port: 13002, + Transport: "websocket", + TransportConfig: proxy.TransportConfig{ + Path: "/ws", + Host: "test.xx.com", + }, + Security: "none", + }, + } +} + +func TestVMess(t *testing.T) { + node := createVMess() + uuid := "123456" + p := buildVMess(node, uuid) + t.Log(p) +} diff --git a/pkg/adapter/surge/default.tpl b/pkg/adapter/surge/default.tpl new file mode 100644 index 0000000..7375b37 --- /dev/null +++ b/pkg/adapter/surge/default.tpl @@ -0,0 +1,61 @@ +#!MANAGED-CONFIG {{ .SubscribeURL }} interval=43200 strict=true +# Surge 的规则配置手册: https://manual.nssurge.com/ + +[General] +loglevel = notify +# 从 Surge iOS 4 / Surge Mac 3.3.0 起,工具开始支持 DoH +doh-server = https://doh.pub/dns-query +# https://dns.alidns.com/dns-query, https://13800000000.rubyfish.cn/, https://dns.google/dns-query +dns-server = 223.5.5.5, 114.114.114.114 +tun-excluded-routes = 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.168.0.0/16, 192.88.99.0/24, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 255.255.255.255/32 +skip-proxy = localhost, *.local, injections.adguard.org, local.adguard.org, captive.apple.com, guzzoni.apple.com, 0.0.0.0/8, 10.0.0.0/8, 17.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.168.0.0/16, 192.88.99.0/24, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 240.0.0.0/4, 255.255.255.255/32 + +wifi-assist = true +allow-wifi-access = true +wifi-access-http-port = 6152 +wifi-access-socks5-port = 6153 +http-listen = 0.0.0.0:6152 +socks5-listen = 0.0.0.0:6153 + +external-controller-access = surgepasswd@0.0.0.0:6170 +replica = false + +tls-provider = openssl +network-framework = false +exclude-simple-hostnames = true +ipv6 = true + +test-timeout = 4 +proxy-test-url = http://www.gstatic.com/generate_204 +geoip-maxmind-url = https://unpkg.zhimg.com/rulestatic@1.0.1/Country.mmdb + +[Replica] +hide-apple-request = true +hide-crashlytics-request = true +use-keyword-filter = false +hide-udp = false + +[Panel] +SubscribeInfo = {{ .SubscribeInfo }}, style=info + +# ----------------------------- +# Surge 的几种策略配置规范,请参考 https://manual.nssurge.com/policy/proxy.html +# 不同的代理策略有*很多*可选参数,请参考上方连接的 Parameters 一段,根据需求自行添加参数。 +# +# Surge 现已支持 UDP 转发功能,请参考: https://trello.com/c/ugOMxD3u/53-udp-%E8%BD%AC%E5%8F%91 +# Surge 现已支持 TCP-Fast-Open 技术,请参考: https://trello.com/c/ij65BU6Q/48-tcp-fast-open-troubleshooting-guide +# Surge 现已支持 ss-libev 的全部加密方式和混淆,请参考: https://trello.com/c/BTr0vG1O/47-ss-libev-%E7%9A%84%E6%94%AF%E6%8C%81%E6%83%85%E5%86%B5 +# ----------------------------- + +[Proxy] +{{ .Proxies }} + +[Proxy Group] +# 代理组列表 +{{ .ProxyGroup }} + +[Rule] +{{ .Rules }} + +[URL Rewrite] +^https?://(www.)?(g|google).cn https://www.google.com 302 \ No newline at end of file diff --git a/pkg/adapter/surge/hysteria2.go b/pkg/adapter/surge/hysteria2.go new file mode 100644 index 0000000..d2205e0 --- /dev/null +++ b/pkg/adapter/surge/hysteria2.go @@ -0,0 +1,43 @@ +package surge + +import ( + "fmt" + "strconv" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func buildHysteria2(data proxy.Proxy, uuid string) string { + hysteria2, ok := data.Option.(proxy.Hysteria2) + if !ok { + return "" + } + + var port int + if hysteria2.HopPorts != "" { + ports := strings.Split(hysteria2.HopPorts, ",") + p := ports[0] + if len(strings.Split(p, "-")) > 1 { + p = strings.Split(p, "-")[0] + } + port, _ = strconv.Atoi(p) + } else { + port = data.Port + } + + config := []string{ + fmt.Sprintf("%s=hysteria2,%s,%d", data.Name, data.Server, port), + "password=" + uuid, + "udp-relay=true", + } + if hysteria2.SecurityConfig.SNI != "" { + config = append(config, "sni="+hysteria2.SecurityConfig.SNI) + } + if hysteria2.SecurityConfig.AllowInsecure { + config = append(config, "skip-cert-verify=true") + } else { + config = append(config, "skip-cert-verify=false") + } + return strings.Join(config, ",") + "\r\n" +} diff --git a/pkg/adapter/surge/hysteria2_test.go b/pkg/adapter/surge/hysteria2_test.go new file mode 100644 index 0000000..73d4306 --- /dev/null +++ b/pkg/adapter/surge/hysteria2_test.go @@ -0,0 +1,70 @@ +package surge + +import ( + "testing" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func TestBuildHysteria2(t *testing.T) { + tests := []struct { + name string + data proxy.Proxy + uuid string + expected string + }{ + { + name: "Valid Hysteria2 with HopPorts", + data: proxy.Proxy{ + Name: "test", + Server: "server.com", + Port: 443, + Option: proxy.Hysteria2{ + HopPorts: "1000-2000", + SecurityConfig: proxy.SecurityConfig{ + SNI: "example.com", + AllowInsecure: true, + }, + }, + }, + uuid: "test-uuid", + expected: "test=hysteria2,server.com,1000,password=test-uuid,udp-relay=true,sni=example.com,skip-cert-verify=true\r\n", + }, + { + name: "Valid Hysteria2 without HopPorts", + data: proxy.Proxy{ + Name: "test", + Server: "server.com", + Port: 443, + Option: proxy.Hysteria2{ + SecurityConfig: proxy.SecurityConfig{ + SNI: "example.com", + AllowInsecure: false, + }, + }, + }, + uuid: "test-uuid", + expected: "test=hysteria2,server.com,443,password=test-uuid,udp-relay=true,sni=example.com,skip-cert-verify=false\r\n", + }, + { + name: "Invalid Hysteria2 Option", + data: proxy.Proxy{ + Name: "test", + Server: "server.com", + Port: 443, + Option: nil, + }, + uuid: "test-uuid", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := buildHysteria2(tt.data, tt.uuid) + if result != tt.expected { + t.Errorf("expected %s, got %s", tt.expected, result) + } + }) + } +} diff --git a/pkg/adapter/surge/shadowsocks.go b/pkg/adapter/surge/shadowsocks.go new file mode 100644 index 0000000..25eef9c --- /dev/null +++ b/pkg/adapter/surge/shadowsocks.go @@ -0,0 +1,24 @@ +package surge + +import ( + "fmt" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func buildShadowsocks(data proxy.Proxy, uuid string) string { + ss, ok := data.Option.(proxy.Shadowsocks) + if !ok { + return "" + } + addr := fmt.Sprintf("%s=ss, %s, %d", data.Name, data.Server, data.Port) + config := []string{ + addr, + fmt.Sprintf("encrypt-method=%s", ss.Method), + fmt.Sprintf("password=%s", uuid), + "tfo=true", + "udp-relay=true", + } + return strings.Join(config, ",") + "\r\n" +} diff --git a/pkg/adapter/surge/surge.go b/pkg/adapter/surge/surge.go new file mode 100644 index 0000000..c6565a5 --- /dev/null +++ b/pkg/adapter/surge/surge.go @@ -0,0 +1,117 @@ +package surge + +import ( + "bytes" + "embed" + "fmt" + "net/url" + "strings" + "text/template" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/traffic" +) + +//go:embed *.tpl +var configFiles embed.FS + +type UserInfo struct { + UUID string + Upload int64 + Download int64 + TotalTraffic int64 + ExpiredDate time.Time + SubscribeURL string +} + +type Surge struct { + Adapter proxy.Adapter + UUID string + User UserInfo +} + +func NewSurge(adapter proxy.Adapter) *Surge { + return &Surge{ + Adapter: adapter, + } +} + +func (m *Surge) Build(uuid, siteName string, user UserInfo) []byte { + var proxies, proxyGroup, rules string + + for _, p := range m.Adapter.Proxies { + switch p.Protocol { + case "shadowsocks": + proxies += buildShadowsocks(p, uuid) + case "trojan": + proxies += buildTrojan(p, uuid) + case "hysteria2": + proxies += buildHysteria2(p, uuid) + case "vmess": + proxies += buildVMess(p, uuid) + } + } + for _, group := range m.Adapter.Group { + if group.Type == proxy.GroupTypeSelect { + proxyGroup += fmt.Sprintf("%s = select, %s", group.Name, strings.Join(group.Proxies, ", ")) + "\r\n" + } else if group.Type == proxy.GroupTypeURLTest { + proxyGroup += fmt.Sprintf("%s = url-test, %s, url=%s, interval=%d", group.Name, strings.Join(group.Proxies, ", "), group.URL, group.Interval) + "\r\n" + } else if group.Type == proxy.GroupTypeFallback { + proxyGroup += fmt.Sprintf("%s = fallback, %s, url=%s, interval=%d", group.Name, strings.Join(group.Proxies, ", "), group.URL, group.Interval) + "\r\n" + } else { + logger.Errorf("[BuildSurfboard] unknown group type: %s", group.Type) + } + } + for _, rule := range m.Adapter.Rules { + if rule == "" { + continue + } + rules += rule + "\r\n" + } + //final rule + rules += "# 最终规则" + "\r\n" + "FINAL,手动选择,dns-failed" + + file, err := configFiles.ReadFile("default.tpl") + if err != nil { + logger.Errorf("read default surfboard config error: %v", err.Error()) + return nil + } + // replace template + tpl, err := template.New("default").Parse(string(file)) + if err != nil { + logger.Errorf("read default surfboard config error: %v", err.Error()) + return nil + } + var buf bytes.Buffer + + var expiredAt string + if user.ExpiredDate.Before(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)) { + expiredAt = "长期有效" + } else { + expiredAt = user.ExpiredDate.Format("2006-01-02 15:04:05") + } + // convert traffic + upload := traffic.AutoConvert(user.Upload, false) + download := traffic.AutoConvert(user.Download, false) + total := traffic.AutoConvert(user.TotalTraffic, false) + unusedTraffic := traffic.AutoConvert(user.TotalTraffic-user.Upload-user.Download, false) + // query Host + urlParse, err := url.Parse(user.SubscribeURL) + if err != nil { + return nil + } + if err := tpl.Execute(&buf, map[string]interface{}{ + "Proxies": proxies, + "ProxyGroup": proxyGroup, + "SubscribeURL": user.SubscribeURL, + "SubscribeInfo": fmt.Sprintf("title=%s订阅信息, content=上传流量:%s\\n下载流量:%s\\n剩余流量: %s\\n套餐流量:%s\\n到期时间:%s", siteName, upload, download, unusedTraffic, total, expiredAt), + "SubscribeDomain": urlParse.Host, + "Rules": rules, + }); err != nil { + logger.Errorf("build Surge config error: %v", err.Error()) + return nil + } + return buf.Bytes() +} diff --git a/pkg/adapter/surge/surge_test.go b/pkg/adapter/surge/surge_test.go new file mode 100644 index 0000000..efec1d5 --- /dev/null +++ b/pkg/adapter/surge/surge_test.go @@ -0,0 +1,97 @@ +package surge + +import ( + "strings" + "testing" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func TestSurgeBuild(t *testing.T) { + adapter := proxy.Adapter{ + Proxies: []proxy.Proxy{ + { + Name: "test-shadowsocks", + Protocol: "shadowsocks", + Server: "1.2.3.4", + Port: 8388, + Option: proxy.Shadowsocks{ + Method: "aes-256-gcm", + }, + }, + { + Name: "test-trojan", + Protocol: "trojan", + Server: "5.6.7.8", + Port: 443, + Option: proxy.Trojan{ + SecurityConfig: proxy.SecurityConfig{ + SNI: "example.com", + AllowInsecure: true, + }, + }, + }, + { + Name: "test-hysteria", + Protocol: "hysteria2", + Server: "1.1.1.1", + Port: 443, + Option: proxy.Hysteria2{ + HopPorts: "8080-8090", + HopInterval: 320, + SecurityConfig: proxy.SecurityConfig{ + SNI: "example.com", + AllowInsecure: true, + }, + }, + }, + }, + Group: []proxy.Group{ + { + Name: "test-group", + Type: proxy.GroupTypeSelect, + Proxies: []string{"test-shadowsocks", "test-trojan", "test-hysteria"}, + }, + { + Name: "手动选择", + Type: proxy.GroupTypeSelect, + Proxies: []string{"test-shadowsocks", "test-trojan", "test-hysteria"}, + }, + }, + Rules: []string{ + "DOMAIN-SUFFIX,example.com,DIRECT", + }, + } + + user := UserInfo{ + UUID: "test-uuid", + Upload: 1024, + Download: 2048, + TotalTraffic: 4096, + ExpiredDate: time.Now().Add(24 * time.Hour), + SubscribeURL: "http://example.com/subscribe", + } + + surge := NewSurge(adapter) + config := surge.Build("test-uuid", "TestSite", user) + + if config == nil { + t.Fatal("Expected non-nil config") + } + + configStr := string(config) + t.Logf("configStr: %v", configStr) + if !strings.Contains(configStr, "test-shadowsocks=ss") { + t.Errorf("Expected config to contain test-shadowsocks proxy") + } + if !strings.Contains(configStr, "test-trojan=trojan") { + t.Errorf("Expected config to contain test-trojan proxy") + } + if !strings.Contains(configStr, "test-group = select") { + t.Errorf("Expected config to contain test-group proxy group") + } + if !strings.Contains(configStr, "DOMAIN-SUFFIX,example.com,DIRECT") { + t.Errorf("Expected config to contain rule for example.com") + } +} diff --git a/pkg/adapter/surge/trojan.go b/pkg/adapter/surge/trojan.go new file mode 100644 index 0000000..4d755c7 --- /dev/null +++ b/pkg/adapter/surge/trojan.go @@ -0,0 +1,32 @@ +package surge + +import ( + "strconv" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func buildTrojan(data proxy.Proxy, uuid string) string { + trojan, ok := data.Option.(proxy.Trojan) + if !ok { + return "" + } + config := []string{ + data.Name + "=trojan", + data.Server, + strconv.Itoa(data.Port), + "password=" + uuid, + "tfo=true", + "udp-relay=true", + } + if trojan.SecurityConfig.SNI != "" { + config = append(config, "sni="+trojan.SecurityConfig.SNI) + } + if trojan.SecurityConfig.AllowInsecure { + config = append(config, "skip-cert-verify=true") + } else { + config = append(config, "skip-cert-verify=false") + } + return strings.Join(config, ",") + "\r\n" +} diff --git a/pkg/adapter/surge/vmess.go b/pkg/adapter/surge/vmess.go new file mode 100644 index 0000000..0b2c95d --- /dev/null +++ b/pkg/adapter/surge/vmess.go @@ -0,0 +1,44 @@ +package surge + +import ( + "fmt" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" +) + +func buildVMess(data proxy.Proxy, uuid string) string { + vmess, ok := data.Option.(proxy.Vmess) + if !ok { + return "" + } + addr := fmt.Sprintf("%s=vmess, %s, %d", data.Name, data.Server, data.Port) + uriConfig := []string{ + addr, + fmt.Sprintf("username=%s", uuid), + "vmess-aead=true", + "tfo=true", + "udp-relay=true", + } + if vmess.Security == "tls" { + uriConfig = append(uriConfig, "tls=true") + if vmess.SecurityConfig.AllowInsecure { + uriConfig = append(uriConfig, "skip-cert-verify=true") + } else { + uriConfig = append(uriConfig, "skip-cert-verify=false") + } + if vmess.SecurityConfig.SNI != "" { + uriConfig = append(uriConfig, fmt.Sprintf("sni=%s", vmess.SecurityConfig.SNI)) + } + } + if vmess.Transport == "websocket" { + uriConfig = append(uriConfig, "ws=true") + if vmess.TransportConfig.Path != "" { + uriConfig = append(uriConfig, fmt.Sprintf("ws-path=%s", vmess.TransportConfig.Path)) + } + if vmess.TransportConfig.Host != "" { + uriConfig = append(uriConfig, fmt.Sprintf("ws-headers=Host:%s", vmess.TransportConfig.Host)) + } + } + return strings.Join(uriConfig, ",") + "\r\n" +} diff --git a/pkg/adapter/uilts.go b/pkg/adapter/uilts.go new file mode 100644 index 0000000..698c1ba --- /dev/null +++ b/pkg/adapter/uilts.go @@ -0,0 +1,197 @@ +package adapter + +import ( + "encoding/json" + "strings" + + "github.com/perfect-panel/ppanel-server/internal/model/server" + "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/random" + "github.com/perfect-panel/ppanel-server/pkg/tool" +) + +func addNode(data *server.Server, host string, port int) *proxy.Proxy { + var option any + node := proxy.Proxy{ + Name: data.Name, + Server: host, + Port: port, + Country: data.Country, + Protocol: data.Protocol, + } + switch data.Protocol { + case "shadowsocks": + var ss proxy.Shadowsocks + if err := json.Unmarshal([]byte(data.Config), &ss); err != nil { + return nil + } + if port == 0 { + node.Port = ss.Port + } + option = ss + case "vless": + var vless proxy.Vless + if err := json.Unmarshal([]byte(data.Config), &vless); err != nil { + return nil + } + if port == 0 { + node.Port = vless.Port + } + option = vless + case "vmess": + var vmess proxy.Vmess + if err := json.Unmarshal([]byte(data.Config), &vmess); err != nil { + return nil + } + if port == 0 { + node.Port = vmess.Port + } + option = vmess + case "trojan": + var trojan proxy.Trojan + if err := json.Unmarshal([]byte(data.Config), &trojan); err != nil { + return nil + } + if port == 0 { + node.Port = trojan.Port + } + option = trojan + case "hysteria2": + var hysteria2 proxy.Hysteria2 + if err := json.Unmarshal([]byte(data.Config), &hysteria2); err != nil { + return nil + } + if port == 0 { + node.Port = hysteria2.Port + } + option = hysteria2 + case "tuic": + var tuic proxy.Tuic + if err := json.Unmarshal([]byte(data.Config), &tuic); err != nil { + return nil + } + if port == 0 { + node.Port = tuic.Port + } + option = tuic + default: + return nil + } + node.Option = option + return &node +} + +func addProxyToGroup(proxyName, groupName string, groups []proxy.Group) []proxy.Group { + for i, group := range groups { + if group.Name == groupName { + groups[i].Proxies = tool.RemoveDuplicateElements(append(group.Proxies, proxyName)...) + return groups + } + } + groups = append(groups, proxy.Group{ + Name: groupName, + Type: "select", + Proxies: []string{proxyName}, + }) + return groups +} + +func adapterRules(groups []*server.RuleGroup) (proxyGroup []proxy.Group, rules []string) { + for _, group := range groups { + proxyGroup = append(proxyGroup, proxy.Group{ + Name: group.Name, + Type: "select", + Proxies: RemoveEmptyString(strings.Split(group.Tags, ",")), + }) + rules = append(rules, strings.Split(group.Rules, "/n")...) + } + return +} + +func generateProxyGroup(servers []proxy.Proxy) (proxyGroup []proxy.Group, region []string) { + // 设置手动选择分组 + proxyGroup = append(proxyGroup, []proxy.Group{ + { + Name: "智能线路", + Type: "url-test", + Proxies: make([]string, 0), + URL: "https://www.gstatic.com/generate_204", + Interval: 300, + }, + { + Name: "手动选择", + Type: "select", + Proxies: []string{"智能线路"}, + }, + }...) + + for _, node := range servers { + if node.Country != "" { + proxyGroup = addProxyToGroup(node.Name, node.Country, proxyGroup) + region = append(region, node.Country) + proxyGroup = addProxyToGroup(node.Country, "智能线路", proxyGroup) + } + proxyGroup = addProxyToGroup(node.Name, "手动选择", proxyGroup) + } + proxyGroup = addProxyToGroup("DIRECT", "手动选择", proxyGroup) + return proxyGroup, tool.RemoveDuplicateElements(region...) +} + +func adapterProxies(servers []*server.Server) []proxy.Proxy { + var proxies []proxy.Proxy + for _, node := range servers { + switch node.RelayMode { + case server.RelayModeAll: + var relays []server.NodeRelay + if err := json.Unmarshal([]byte(node.RelayNode), &relays); err != nil { + logger.Errorw("Unmarshal RelayNode", logger.Field("error", err.Error()), logger.Field("node", node.Name), logger.Field("relayNode", node.RelayNode)) + continue + } + for _, relay := range relays { + n := addNode(node, relay.Host, relay.Port) + if n == nil { + continue + } + if relay.Prefix != "" { + n.Name = relay.Prefix + "-" + n.Name + } + proxies = append(proxies, *n) + } + case server.RelayModeRandom: + var relays []server.NodeRelay + if err := json.Unmarshal([]byte(node.RelayNode), &relays); err != nil { + logger.Errorw("Unmarshal RelayNode", logger.Field("error", err.Error()), logger.Field("node", node.Name), logger.Field("relayNode", node.RelayNode)) + continue + } + randNum := random.RandomInRange(0, len(relays)-1) + relay := relays[randNum] + n := addNode(node, relay.Host, relay.Port) + if n == nil { + continue + } + if relay.Prefix != "" { + n.Name = relay.Prefix + " - " + node.Name + } + proxies = append(proxies, *n) + default: + logger.Info("Not Relay Mode", logger.Field("node", node.Name), logger.Field("relayMode", node.RelayMode)) + n := addNode(node, node.ServerAddr, 0) + if n != nil { + proxies = append(proxies, *n) + } + } + } + return proxies +} + +// RemoveEmptyString 切片去除空值 +func RemoveEmptyString(arr []string) []string { + var result []string + for _, str := range arr { + if str != "" { + result = append(result, str) + } + } + return result +} diff --git a/pkg/aes/aes.go b/pkg/aes/aes.go new file mode 100644 index 0000000..bc68767 --- /dev/null +++ b/pkg/aes/aes.go @@ -0,0 +1,47 @@ +package pkgaes + +import ( + "crypto/md5" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" + "time" + + "github.com/forgoer/openssl" +) + +// Encrypt 传入 []byte,返回 []byte 类型的加密数据 +func Encrypt(plainText []byte, keyStr string) (string, string, error) { + //get time + nonce := fmt.Sprintf("%x", time.Now().UnixNano()) + key := generateKey(keyStr) + iv := generateIv(nonce, keyStr) + dst, err := openssl.AesCBCEncrypt(plainText, key, iv, openssl.PKCS7_PADDING) + // 返回加密后的数据(包括 IV) + return base64.StdEncoding.EncodeToString(dst), nonce, err +} + +// Decrypt 传入 []byte 类型的加密数据,返回解密后的 []byte 明文数据 +func Decrypt(cipherText string, keyStr string, ivStr string) (string, error) { + decode, err := base64.StdEncoding.DecodeString(cipherText) + if err != nil { + return "", err + } + key := generateKey(keyStr) + iv := generateIv(ivStr, keyStr) + dst, err := openssl.AesCBCDecrypt(decode, key, iv, openssl.PKCS7_PADDING) + return string(dst), err +} + +// 生成密钥(哈希处理后保持为固定大小) +func generateKey(key string) []byte { + hash := sha256.Sum256([]byte(key)) + return hash[:32] // AES-256 需要 32 字节密钥 +} + +func generateIv(iv, key string) []byte { + h := md5.New() + h.Write([]byte(iv)) + return generateKey(hex.EncodeToString(h.Sum(nil)) + key) +} diff --git a/pkg/aes/aes_test.go b/pkg/aes/aes_test.go new file mode 100644 index 0000000..286173e --- /dev/null +++ b/pkg/aes/aes_test.go @@ -0,0 +1,29 @@ +package pkgaes + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAes(t *testing.T) { + params := map[string]interface{}{ + "method": "email", + "account": "admin@ppanel.dev", + "password": "password", + } + marshal, _ := json.Marshal(params) + jsonStr := string(marshal) + encrypt, iv, err := Encrypt([]byte(jsonStr), "123456") + if err != nil { + t.Fatalf("encrypt failed: %v", err) + } + decrypt, err := Decrypt(encrypt, "123456", iv) + if err != nil { + t.Fatalf("decrypt failed: %v", err) + } + + assert.Equal(t, jsonStr, decrypt, "decrypt failed") + +} diff --git a/pkg/authmethod/authmethod.go b/pkg/authmethod/authmethod.go new file mode 100644 index 0000000..d02ce91 --- /dev/null +++ b/pkg/authmethod/authmethod.go @@ -0,0 +1,8 @@ +package authmethod + +const ( + Email = "email" //邮箱 + Mobile = "mobile" //手机 + Device = "device" //设备 + +) diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go new file mode 100644 index 0000000..64c68dc --- /dev/null +++ b/pkg/cache/cache.go @@ -0,0 +1,27 @@ +package cache + +import ( + "context" + "time" +) + +type ( + Cache interface { + // Del deletes cached values with keys. + Del(keys ...string) error + // DelCtx deletes cached values with keys. + DelCtx(ctx context.Context, keys ...string) error + // Get gets the cache with key and fills into v. + Get(key string, val any) error + // GetCtx gets the cache with key and fills into v. + GetCtx(ctx context.Context, key string, val any) error + // Set sets the cache with key and value. + Set(key string, val any) error + // SetCtx sets the cache with key and value. + SetCtx(ctx context.Context, key string, val any) error + // SetWithExpire sets the cache with key and v, using given expire. + SetWithExpire(key string, val any, expire time.Duration) error + // SetWithExpireCtx sets the cache with key and v, using given expire. + SetWithExpireCtx(ctx context.Context, key string, val any, expire time.Duration) error + } +) diff --git a/pkg/cache/gorm.go b/pkg/cache/gorm.go new file mode 100644 index 0000000..23552de --- /dev/null +++ b/pkg/cache/gorm.go @@ -0,0 +1,120 @@ +package cache + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var ErrNotFound = redis.Nil + +type ( + // ExecCtxFn defines the sql exec method. + ExecCtxFn func(conn *gorm.DB) error + // IndexQueryCtxFn defines the query method that based on unique indexes. + IndexQueryCtxFn func(conn *gorm.DB, v interface{}) (interface{}, error) + // PrimaryQueryCtxFn defines the query method that based on primary keys. + PrimaryQueryCtxFn func(conn *gorm.DB, v, primary interface{}) error + // QueryCtxFn defines the query method. + QueryCtxFn func(conn *gorm.DB, v interface{}) error + + CachedConn struct { + db *gorm.DB + cache *redis.Client + } +) + +// NewConn returns a CachedConn with a redis cluster cache. +func NewConn(db *gorm.DB, c *redis.Client) CachedConn { + return CachedConn{ + db: db, + cache: c, + } +} + +// DelCache deletes cache with keys. +func (cc CachedConn) DelCache(keys ...string) error { + return cc.cache.Del(context.Background(), keys...).Err() +} + +// DelCacheCtx deletes cache with keys. +func (cc CachedConn) DelCacheCtx(ctx context.Context, keys ...string) error { + return cc.cache.Del(ctx, keys...).Err() +} + +// GetCache unmarshals cache with given key into v. +func (cc CachedConn) GetCache(key string, v interface{}) error { + // query redis key + val, err := cc.cache.Get(context.Background(), key).Result() + if err != nil { + return err + } + // unmarshal value + return json.Unmarshal([]byte(val), v) +} + +// SetCache sets cache with key and v. +func (cc CachedConn) SetCache(key string, v interface{}) error { + // marshal value + val, err := json.Marshal(v) + if err != nil { + return err + } + // set redis key + return cc.cache.Set(context.Background(), key, val, 0).Err() +} + +// ExecCtx runs given exec on given keys, and returns execution result. +func (cc CachedConn) ExecCtx(ctx context.Context, execCtx ExecCtxFn, keys ...string) error { + err := execCtx(cc.db.WithContext(ctx)) + if err != nil { + return err + } + if err := cc.DelCacheCtx(ctx, keys...); err != nil { + return err + } + return nil +} + +// ExecNoCache runs exec with given sql statement, without affecting cache. +func (cc CachedConn) ExecNoCache(exec ExecCtxFn) error { + return cc.ExecNoCacheCtx(context.Background(), exec) +} + +// ExecNoCacheCtx runs exec with given sql statement, without affecting cache. +func (cc CachedConn) ExecNoCacheCtx(ctx context.Context, execCtx ExecCtxFn) (err error) { + return execCtx(cc.db.WithContext(ctx)) +} + +func (cc CachedConn) QueryCtx(ctx context.Context, v interface{}, key string, query QueryCtxFn) (err error) { + err = cc.GetCache(key, v) + if err != nil { + if errors.Is(err, ErrNotFound) { + err = query(cc.db.WithContext(ctx), v) + if err != nil { + return err + } + return cc.SetCache(key, v) + } + } + return +} + +// QueryNoCacheCtx runs query with given sql statement, without affecting cache. +func (cc CachedConn) QueryNoCacheCtx(ctx context.Context, v interface{}, query QueryCtxFn) (err error) { + return query(cc.db.WithContext(ctx), v) +} + +// TransactCtx runs given fn in transaction mode. +func (cc CachedConn) TransactCtx(ctx context.Context, fn func(db *gorm.DB) error, opts ...*sql.TxOptions) error { + return cc.db.WithContext(ctx).Transaction(fn, opts...) +} + +// Transact runs given fn in transaction mode. +func (cc CachedConn) Transact(fn func(db *gorm.DB) error, opts ...*sql.TxOptions) error { + return cc.TransactCtx(context.Background(), fn, opts...) +} diff --git a/pkg/cache/gorm_test.go b/pkg/cache/gorm_test.go new file mode 100644 index 0000000..76bf62e --- /dev/null +++ b/pkg/cache/gorm_test.go @@ -0,0 +1,66 @@ +package cache + +import ( + "context" + "testing" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/orm" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" + "gorm.io/plugin/soft_delete" +) + +type User struct { + Id int64 `gorm:"primarykey"` + Email string `gorm:"index:idx_email;type:varchar(100);unique;not null;comment:电子邮箱"` + Password string `gorm:"type:varchar(100);comment:用户密码;not null"` + Avatar string `gorm:"type:varchar(200);default:'';comment:用户头像"` + Balance int64 `gorm:"default:0;comment:用户余额"` + Telegram int64 `gorm:"default:null;comment:Telegram账号"` + ReferCode string `gorm:"type:varchar(20);default:'';comment:推荐码"` + RefererId int64 `gorm:"comment:推荐人ID"` + Enable bool `gorm:"default:true;not null;comment:账户是否可用"` + IsAdmin bool `gorm:"default:false;not null;comment:是否管理员"` + ValidEmail bool `gorm:"default:false;not null;comment:是否验证邮箱"` + EnableEmailNotify bool `gorm:"default:false;not null;comment:是否启用邮件通知"` + EnableTelegramNotify bool `gorm:"default:false;not null;comment:是否启用Telegram通知"` + EnableBalanceNotify bool `gorm:"default:false;not null;comment:是否启用余额变动通知"` + EnableLoginNotify bool `gorm:"default:false;not null;comment:是否启用登录通知"` + EnableSubscribeNotify bool `gorm:"default:false;not null;comment:是否启用订阅通知"` + EnableTradeNotify bool `gorm:"default:false;not null;comment:是否启用交易通知"` + CreatedAt time.Time `gorm:"<-:create;comment:创建时间"` + UpdatedAt time.Time `gorm:"comment:更新时间"` + DeletedAt gorm.DeletedAt `gorm:"default:null;comment:删除时间"` + IsDel soft_delete.DeletedAt `gorm:"softDelete:flag,DeletedAtField:DeletedAt;comment:1:正常 0:删除"` // Use `1` `0` to identify +} + +func TestGormCacheCtx(t *testing.T) { + t.Skipf("skip TestGormCacheCtx test") + db, err := orm.ConnectMysql(orm.Mysql{ + Config: orm.Config{ + Addr: "localhost:3306", + Config: "charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai", + Dbname: "vpnboard", + Username: "root", + Password: "mylove520", + }, + }) + if err != nil { + t.Error(err) + } + rds := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + }) + conn := NewConn(db, rds) + var u User + key := "user:id" + err = conn.QueryCtx(context.Background(), &u, key, func(conn *gorm.DB, v interface{}) error { + return conn.Where("id = ?", 1).First(v).Error + }) + if err != nil { + t.Error(err) + return + } + t.Logf("get cache success %+v", u) +} diff --git a/pkg/calculateMonths/calculateMonths.go b/pkg/calculateMonths/calculateMonths.go new file mode 100644 index 0000000..e3ffc95 --- /dev/null +++ b/pkg/calculateMonths/calculateMonths.go @@ -0,0 +1,17 @@ +package calculateMonths + +import "time" + +// CalculateMonths calculates the number of months between startTime and endTime. +// It rounds up to the next month if there are remaining days. +func CalculateMonths(startTime, endTime time.Time) int8 { + // Calculate the year and month difference + years := endTime.Year() - startTime.Year() + months := int8(years*12) + int8(endTime.Month()) - int8(startTime.Month()) + + // Always round up if endTime is not on the same or earlier day of the month + if endTime.Day() > startTime.Day() || (endTime.Day() < startTime.Day() && endTime.After(startTime)) { + months++ + } + return months +} diff --git a/pkg/calculateMonths/calculateMonths_test.go b/pkg/calculateMonths/calculateMonths_test.go new file mode 100644 index 0000000..77fb43a --- /dev/null +++ b/pkg/calculateMonths/calculateMonths_test.go @@ -0,0 +1,13 @@ +package calculateMonths + +import ( + "testing" + "time" +) + +func TestCalculateMonths(t *testing.T) { + startTime, _ := time.Parse(time.DateTime, "2025-01-15 00:00:00") + EndTime, _ := time.Parse(time.DateTime, "2025-05-15 00:00:00") + months := CalculateMonths(startTime, EndTime) + t.Log(months) +} diff --git a/pkg/color/color.go b/pkg/color/color.go new file mode 100644 index 0000000..6e892d2 --- /dev/null +++ b/pkg/color/color.go @@ -0,0 +1,73 @@ +package color + +import "github.com/fatih/color" + +const ( + // NoColor is no color for both foreground and background. + NoColor Color = iota + // FgBlack is the foreground color black. + FgBlack + // FgRed is the foreground color red. + FgRed + // FgGreen is the foreground color green. + FgGreen + // FgYellow is the foreground color yellow. + FgYellow + // FgBlue is the foreground color blue. + FgBlue + // FgMagenta is the foreground color magenta. + FgMagenta + // FgCyan is the foreground color cyan. + FgCyan + // FgWhite is the foreground color white. + FgWhite + + // BgBlack is the background color black. + BgBlack + // BgRed is the background color red. + BgRed + // BgGreen is the background color green. + BgGreen + // BgYellow is the background color yellow. + BgYellow + // BgBlue is the background color blue. + BgBlue + // BgMagenta is the background color magenta. + BgMagenta + // BgCyan is the background color cyan. + BgCyan + // BgWhite is the background color white. + BgWhite +) + +var colors = map[Color][]color.Attribute{ + FgBlack: {color.FgBlack, color.Bold}, + FgRed: {color.FgRed, color.Bold}, + FgGreen: {color.FgGreen, color.Bold}, + FgYellow: {color.FgYellow, color.Bold}, + FgBlue: {color.FgBlue, color.Bold}, + FgMagenta: {color.FgMagenta, color.Bold}, + FgCyan: {color.FgCyan, color.Bold}, + FgWhite: {color.FgWhite, color.Bold}, + BgBlack: {color.BgBlack, color.FgHiWhite, color.Bold}, + BgRed: {color.BgRed, color.FgHiWhite, color.Bold}, + BgGreen: {color.BgGreen, color.FgHiWhite, color.Bold}, + BgYellow: {color.BgHiYellow, color.FgHiBlack, color.Bold}, + BgBlue: {color.BgBlue, color.FgHiWhite, color.Bold}, + BgMagenta: {color.BgMagenta, color.FgHiWhite, color.Bold}, + BgCyan: {color.BgCyan, color.FgHiWhite, color.Bold}, + BgWhite: {color.BgHiWhite, color.FgHiBlack, color.Bold}, +} + +type Color uint32 + +// WithColor returns a string with the given color applied. +func WithColor(text string, colour Color) string { + c := color.New(colors[colour]...) + return c.Sprint(text) +} + +// WithColorPadding returns a string with the given color applied with leading and trailing spaces. +func WithColorPadding(text string, colour Color) string { + return WithColor(" "+text+" ", colour) +} diff --git a/pkg/color/color_test.go b/pkg/color/color_test.go new file mode 100644 index 0000000..74ee2a1 --- /dev/null +++ b/pkg/color/color_test.go @@ -0,0 +1,17 @@ +package color + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestWithColor(t *testing.T) { + output := WithColor("Hello", BgRed) + assert.Equal(t, "Hello", output) +} + +func TestWithColorPadding(t *testing.T) { + output := WithColorPadding("Hello", BgRed) + assert.Equal(t, " Hello ", output) +} diff --git a/pkg/conf/config.go b/pkg/conf/config.go new file mode 100644 index 0000000..265108a --- /dev/null +++ b/pkg/conf/config.go @@ -0,0 +1,28 @@ +package conf + +import ( + "log" + "os" + + "gopkg.in/yaml.v3" +) + +func MustLoad(file string, v any) { + if err := Load(file, v); err != nil { + log.Fatalf("error: config file %s, %s", file, err.Error()) + } +} + +func Load(file string, v any) error { + setDefaults(v) + content, err := os.ReadFile(file) + if err != nil { + return err + } + + // Unmarshal the YAML content directly into the target structure + if err := yaml.Unmarshal(content, v); err != nil { + return err + } + return nil +} diff --git a/pkg/conf/config_test.go b/pkg/conf/config_test.go new file mode 100644 index 0000000..2070e88 --- /dev/null +++ b/pkg/conf/config_test.go @@ -0,0 +1,18 @@ +package conf + +import "testing" + +type Server struct { + Host string `yaml:"Host" default:"localhost"` + Port int `yaml:"Port" default:"8080"` +} + +type Config struct { + Server Server `yaml:"Server"` +} + +func TestConfigLoad(t *testing.T) { + var c Config + MustLoad("./config_test.yaml", &c) + t.Logf("config: %+v", c) +} diff --git a/pkg/conf/config_test.yaml b/pkg/conf/config_test.yaml new file mode 100644 index 0000000..bcef5fc --- /dev/null +++ b/pkg/conf/config_test.yaml @@ -0,0 +1,3 @@ +Server: + Port: 9999 + Host: 0.0.0.0 diff --git a/pkg/conf/default.go b/pkg/conf/default.go new file mode 100644 index 0000000..9c6af19 --- /dev/null +++ b/pkg/conf/default.go @@ -0,0 +1,63 @@ +package conf + +import ( + "fmt" + "reflect" +) + +func setDefaults(v any) { + // Get the element of the pointer + val := reflect.ValueOf(v).Elem() + setDefaultsRecursive(val) +} +func setDefaultsRecursive(v reflect.Value) { + if v.Kind() != reflect.Struct { + return + } + typ := v.Type() + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + fieldType := typ.Field(i) + // if the field is a struct, set recursively + if field.Kind() == reflect.Struct { + setDefaultsRecursive(field) + } + + // if the field is zero value and has default tag, set the default value + if isZero(field) { + defaultValue := fieldType.Tag.Get("default") + if defaultValue != "" { + // set the value for the field using reflection + field.Set(reflect.ValueOf(parseDefaultValue(field.Kind(), defaultValue))) + } + } + } +} +func isZero(v reflect.Value) bool { + return reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface()) +} +func parseDefaultValue(kind reflect.Kind, defaultValue string) any { + switch kind { + case reflect.String: + return defaultValue + case reflect.Int: + var i int + _, _ = fmt.Sscanf(defaultValue, "%d", &i) + return i + case reflect.Int64: + var i int64 + _, _ = fmt.Sscanf(defaultValue, "%d", &i) + return i + case reflect.Bool: + var b bool + _, _ = fmt.Sscanf(defaultValue, "%t", &b) + return b + case reflect.Uint32: + var i uint32 + _, _ = fmt.Sscanf(defaultValue, "%d", &i) + return i + default: + fmt.Printf("类型 %v 没有处理, 值为: %v \n", kind, defaultValue) + panic("unhandled default case") + } +} diff --git a/pkg/constant/context.go b/pkg/constant/context.go new file mode 100644 index 0000000..4023cd1 --- /dev/null +++ b/pkg/constant/context.go @@ -0,0 +1,11 @@ +package constant + +type CtxKey string + +const ( + CtxKeyUser CtxKey = "user" + CtxKeySessionID CtxKey = "sessionId" + CtxKeyRequestHost CtxKey = "requestHost" + CtxKeyPlatform CtxKey = "platform" + CtxKeyPayment CtxKey = "payment" +) diff --git a/pkg/constant/types.go b/pkg/constant/types.go new file mode 100644 index 0000000..20238a9 --- /dev/null +++ b/pkg/constant/types.go @@ -0,0 +1,51 @@ +package constant + +import ( + "encoding/json" +) + +// Used for type cloning conversion +const ( + Int64 int64 = 0 + Uint32 uint32 = 0 + DevMode = "dev" +) + +// VerifyType is the type of verification code +type VerifyType uint8 + +const ( + Register VerifyType = iota + 1 + Security +) + +func ParseVerifyType(i uint8) VerifyType { + return VerifyType(i) +} + +func (v VerifyType) String() string { + switch v { + case Register: + return "register" + case Security: + return "security" + default: + return "unknown" + } +} + +// TempOrderCacheKey Cache to Redis Key +// eg: temp_order:order_no +const TempOrderCacheKey = "temp_order:%s" + +type TemporaryOrderInfo struct { + OrderNo string `json:"order_no"` + Identifier string `json:"identifier"` + AuthType string `json:"auth_type"` + Password string `json:"password"` +} + +func (t TemporaryOrderInfo) Marshal() string { + value, _ := json.Marshal(t) + return string(value) +} diff --git a/pkg/constant/version.go b/pkg/constant/version.go new file mode 100644 index 0000000..a964009 --- /dev/null +++ b/pkg/constant/version.go @@ -0,0 +1,4 @@ +package constant + +// Version PPanel version +const Version = "0.3.0(3002)" diff --git a/pkg/countryCenter/county_center.go b/pkg/countryCenter/county_center.go new file mode 100644 index 0000000..549b762 --- /dev/null +++ b/pkg/countryCenter/county_center.go @@ -0,0 +1,1175 @@ +package countryCenter + +import ( + "fmt" + "strings" +) + +var countryCenter = map[string][2]float64{ + "Afghanistan": {33.93911, 67.709953}, + "Albania": {41.153332, 20.168331}, + "Algeria": {28.033886, 1.659626}, + "Andorra": {42.546245, 1.601554}, + "Angola": {-11.202692, 17.873887}, + "Antigua and Barbuda": {17.060816, -61.796428}, + "Argentina": {-38.416097, -63.616672}, + "Armenia": {40.069099, 45.038189}, + "Australia": {-25.274398, 133.775136}, + "Azerbaijan": {40.143105, 47.576927}, + "Bahamas": {25.03428, -77.39628}, + "Bahrain": {26.0667, 50.5577}, + "Bangladesh": {23.684994, 90.356331}, + "Barbados": {13.193887, -59.543198}, + "Belarus": {53.709807, 27.953389}, + "Belgium": {50.503887, 4.469936}, + "Belize": {17.189877, -88.49765}, + "Benin": {9.30769, 2.315834}, + "Bhutan": {27.514162, 90.433601}, + "Bolivia": {-16.290154, -63.588653}, + "Bosnia and Herzegovina": {43.915886, 17.679076}, + "Botswana": {-22.328474, 24.684866}, + "Brazil": {-14.235004, -51.92528}, + "Brunei": {4.535277, 114.727669}, + "Bulgaria": {42.733883, 25.48583}, + "Burkina Faso": {12.238333, -1.561593}, + "Burundi": {-3.373056, 29.918886}, + "Cabo Verde": {16.5388, -23.0418}, + "Cambodia": {12.565679, 104.990963}, + "Cameroon": {7.369722, 12.354722}, + "Canada": {56.130366, -106.346771}, + "Central African Republic": {6.611111, 20.939444}, + "Chad": {15.454166, 18.732207}, + "Chile": {-35.675147, -71.542969}, + "China": {35.86166, 104.195397}, + "Colombia": {4.570868, -74.297333}, + "Comoros": {-11.6455, 43.3333}, + "Congo": {-0.228021, 15.827659}, + "Costa Rica": {9.748917, -83.753428}, + "Croatia": {45.1, 15.2}, + "Cuba": {21.521757, -77.781167}, + "Cyprus": {35.126413, 33.429859}, + "Czechia": {49.817492, 15.472962}, + "Denmark": {56.26392, 9.501785}, + "Djibouti": {11.825138, 42.590275}, + "Dominica": {15.414999, -61.370976}, + "Dominican Republic": {18.735693, -70.162651}, + "Ecuador": {-1.831239, -78.183406}, + "Egypt": {26.820553, 30.802498}, + "El Salvador": {13.794185, -88.89653}, + "Equatorial Guinea": {1.650801, 10.267895}, + "Eritrea": {15.179384, 39.782334}, + "Estonia": {58.595272, 25.013607}, + "Eswatini": {-26.522503, 31.465866}, + "Ethiopia": {9.145, 40.489673}, + "Fiji": {-17.713371, 178.065032}, + "Finland": {61.92411, 25.748151}, + "France": {46.603354, 1.888334}, + "Gabon": {-0.803689, 11.609444}, + "Gambia": {13.443182, -15.310139}, + "Georgia": {32.165622, -82.900075}, + "Germany": {51.165691, 10.451526}, + "Ghana": {7.946527, -1.023194}, + "Greece": {39.074208, 21.824312}, + "Grenada": {12.262776, -61.604171}, + "Guatemala": {15.783471, -90.230759}, + "Guinea": {9.945587, -9.696645}, + "Guinea-Bissau": {11.803749, -15.180413}, + "Guyana": {4.860416, -58.93018}, + "Haiti": {18.971187, -72.285215}, + "Honduras": {15.199999, -86.241905}, + "Hungary": {47.162494, 19.503304}, + "Iceland": {64.963051, -19.020835}, + "India": {20.593684, 78.96288}, + "Indonesia": {-0.789275, 113.921327}, + "Iran": {32.427908, 53.688046}, + "Iraq": {33.223191, 43.679291}, + "Ireland": {53.41291, -8.24389}, + "Israel": {31.046051, 34.851612}, + "Italy": {41.87194, 12.56738}, + "Jamaica": {18.109581, -77.297508}, + "Japan": {36.204824, 138.252924}, + "Jordan": {30.585164, 36.238414}, + "Kazakhstan": {48.019573, 66.923684}, + "Kenya": {-1.292066, 36.821946}, + "Kiribati": {-3.370417, -168.734039}, + "Kuwait": {29.31166, 47.481766}, + "Kyrgyzstan": {41.20438, 74.766098}, + "Laos": {19.85627, 102.495496}, + "Latvia": {56.879635, 24.603189}, + "Lebanon": {33.854721, 35.862285}, + "Lesotho": {-29.609988, 28.233608}, + "Liberia": {6.428055, -9.429499}, + "Libya": {26.3351, 17.228331}, + "Liechtenstein": {47.166, 9.555373}, + "Lithuania": {55.169438, 23.881275}, + "Luxembourg": {49.815273, 6.129583}, + "Madagascar": {-18.766947, 46.869107}, + "Malawi": {-13.254308, 34.301525}, + "Malaysia": {4.210484, 101.975766}, + "Maldives": {3.202778, 73.22068}, + "Mali": {17.570692, -3.996166}, + "Malta": {35.937496, 14.375416}, + "Marshall Islands": {7.131474, 171.184478}, + "Mauritania": {21.00789, -10.940835}, + "Mauritius": {-20.348404, 57.552152}, + "Mexico": {23.634501, -102.552784}, + "Micronesia": {7.425554, 150.550812}, + "Moldova": {47.411631, 28.369885}, + "Monaco": {43.738417, 7.424616}, + "Mongolia": {46.862496, 103.846656}, + "Montenegro": {42.708678, 19.37439}, + "Morocco": {31.791702, -7.09262}, + "Mozambique": {-18.665695, 35.529562}, + "Myanmar": {21.916221, 95.955974}, + "Namibia": {-22.95764, 18.49041}, + "Nauru": {-0.522778, 166.931503}, + "Nepal": {28.394857, 84.124008}, + "Netherlands": {52.132633, 5.291266}, + "New Zealand": {-40.900557, 174.885971}, + "Nicaragua": {12.865416, -85.207229}, + "Niger": {17.607789, 8.081666}, + "Nigeria": {9.081999, 8.675277}, + "North Korea": {40.339852, 127.510093}, + "North Macedonia": {41.608635, 21.745275}, + "Norway": {60.472024, 8.468946}, + "Oman": {21.512583, 55.923255}, + "Pakistan": {30.375321, 69.345116}, + "Palau": {7.51498, 134.58252}, + "Palestine": {31.952162, 35.233154}, + "Panama": {8.537981, -80.782127}, + "Papua New Guinea": {-6.314993, 143.95555}, + "Paraguay": {-23.442503, -58.443832}, + "Peru": {-9.189967, -75.015152}, + "Philippines": {12.879721, 121.774017}, + "Poland": {51.919438, 19.145136}, + "Portugal": {39.399872, -8.224454}, + "Qatar": {25.354826, 51.183884}, + "Romania": {45.943161, 24.96676}, + "Russia": {61.52401, 105.318756}, + "Rwanda": {-1.940278, 29.873888}, + "Saint Kitts and Nevis": {17.357822, -62.782998}, + "Saint Lucia": {13.909444, -60.978893}, + "Saint Vincent and the Grenadines": {12.984305, -61.287228}, + "Samoa": {-13.759029, -172.104629}, + "San Marino": {43.94236, 12.457777}, + "Sao Tome and Principe": {0.18636, 6.613081}, + "Saudi Arabia": {23.885942, 45.079162}, + "Senegal": {14.497401, -14.452362}, + "Serbia": {44.016521, 21.005859}, + "Seychelles": {-4.679574, 55.491977}, + "Sierra Leone": {8.460555, -11.779889}, + "Singapore": {1.352083, 103.819836}, + "Slovakia": {48.669026, 19.699024}, + "Slovenia": {46.151241, 14.995463}, + "Solomon Islands": {-9.64571, 160.156194}, + "Somalia": {5.152149, 46.199616}, + "South Africa": {-30.559482, 22.937506}, + "South Korea": {35.907757, 127.766922}, + "South Sudan": {6.8769919, 31.3069788}, + "Spain": {40.463667, -3.74922}, + "Sri Lanka": {7.873054, 80.771797}, + "Sudan": {12.862807, 30.217636}, + "Suriname": {3.919305, -56.027783}, + "Sweden": {60.128161, 18.643501}, + "Switzerland": {46.818188, 8.227512}, + "Syria": {34.802075, 38.996815}, + "Tajikistan": {38.861034, 71.276093}, + "Tanzania": {-6.369028, 34.888822}, + "Thailand": {15.870032, 100.992541}, + "Timor-Leste": {-8.874217, 125.727539}, + "Togo": {8.619543, 0.824782}, + "Tonga": {-21.178986, -175.198242}, + "Trinidad and Tobago": {10.691803, -61.222503}, + "Tunisia": {33.886917, 9.537499}, + "Turkey": {38.963745, 35.243322}, + "Turkmenistan": {38.969719, 59.556278}, + "Tuvalu": {-7.109535, 177.64933}, + "Uganda": {1.373333, 32.290275}, + "Ukraine": {48.379433, 31.16558}, + "United Arab Emirates": {23.424076, 53.847818}, + "United Kingdom": {55.378051, -3.435973}, + "United States": {37.09024, -95.712891}, + "Uruguay": {-32.522779, -55.765835}, + "Uzbekistan": {41.377491, 64.585262}, + "Vanuatu": {-15.376706, 166.959158}, + "Vatican City": {41.902916, 12.453389}, + "Venezuela": {6.42375, -66.58973}, + "Vietnam": {14.058324, 108.277199}, + "Yemen": {15.552727, 48.516388}, + "Zambia": {-13.133897, 27.849332}, + "Zimbabwe": {-19.015438, 29.154857}, +} + +// 国家简称到全称的映射表 +var countryAbbr = map[string]string{ + // ISO 3166-1 alpha-2 codes + "AD": "Andorra", + "AE": "United Arab Emirates", + "AF": "Afghanistan", + "AG": "Antigua and Barbuda", + "AI": "Anguilla", + "AL": "Albania", + "AM": "Armenia", + "AO": "Angola", + "AQ": "Antarctica", + "AR": "Argentina", + "AS": "American Samoa", + "AT": "Austria", + "AU": "Australia", + "AW": "Aruba", + "AX": "Aland Islands", + "AZ": "Azerbaijan", + "BA": "Bosnia and Herzegovina", + "BB": "Barbados", + "BD": "Bangladesh", + "BE": "Belgium", + "BF": "Burkina Faso", + "BG": "Bulgaria", + "BH": "Bahrain", + "BI": "Burundi", + "BJ": "Benin", + "BL": "Saint Barthelemy", + "BM": "Bermuda", + "BN": "Brunei", + "BO": "Bolivia", + "BQ": "Bonaire", + "BR": "Brazil", + "BS": "Bahamas", + "BT": "Bhutan", + "BV": "Bouvet Island", + "BW": "Botswana", + "BY": "Belarus", + "BZ": "Belize", + "CA": "Canada", + "CC": "Cocos Islands", + "CD": "Congo", + "CF": "Central African Republic", + "CG": "Congo", + "CH": "Switzerland", + "CI": "Cote D'Ivoire", + "CK": "Cook Islands", + "CL": "Chile", + "CM": "Cameroon", + "CN": "China", + "CO": "Colombia", + "CR": "Costa Rica", + "CU": "Cuba", + "CV": "Cabo Verde", + "CW": "Curacao", + "CX": "Christmas Island", + "CY": "Cyprus", + "CZ": "Czechia", + "DE": "Germany", + "DJ": "Djibouti", + "DK": "Denmark", + "DM": "Dominica", + "DO": "Dominican Republic", + "DZ": "Algeria", + "EC": "Ecuador", + "EE": "Estonia", + "EG": "Egypt", + "EH": "Western Sahara", + "ER": "Eritrea", + "ES": "Spain", + "ET": "Ethiopia", + "FI": "Finland", + "FJ": "Fiji", + "FK": "Falkland Islands", + "FM": "Micronesia", + "FO": "Faroe Islands", + "FR": "France", + "GA": "Gabon", + "GB": "United Kingdom", + "GD": "Grenada", + "GE": "Georgia", + "GF": "French Guiana", + "GG": "Guernsey", + "GH": "Ghana", + "GI": "Gibraltar", + "GL": "Greenland", + "GM": "Gambia", + "GN": "Guinea", + "GP": "Guadeloupe", + "GQ": "Equatorial Guinea", + "GR": "Greece", + "GS": "South Georgia", + "GT": "Guatemala", + "GU": "Guam", + "GW": "Guinea-Bissau", + "GY": "Guyana", + "HK": "Hong Kong", + "HM": "Heard Island", + "HN": "Honduras", + "HR": "Croatia", + "HT": "Haiti", + "HU": "Hungary", + "ID": "Indonesia", + "IE": "Ireland", + "IL": "Israel", + "IM": "Isle of Man", + "IN": "India", + "IO": "British Indian Ocean Territory", + "IQ": "Iraq", + "IR": "Iran", + "IS": "Iceland", + "IT": "Italy", + "JE": "Jersey", + "JM": "Jamaica", + "JO": "Jordan", + "JP": "Japan", + "KE": "Kenya", + "KG": "Kyrgyzstan", + "KH": "Cambodia", + "KI": "Kiribati", + "KM": "Comoros", + "KN": "Saint Kitts and Nevis", + "KP": "North Korea", + "KR": "South Korea", + "KW": "Kuwait", + "KY": "Cayman Islands", + "KZ": "Kazakhstan", + "LA": "Laos", + "LB": "Lebanon", + "LC": "Saint Lucia", + "LI": "Liechtenstein", + "LK": "Sri Lanka", + "LR": "Liberia", + "LS": "Lesotho", + "LT": "Lithuania", + "LU": "Luxembourg", + "LV": "Latvia", + "LY": "Libya", + "MA": "Morocco", + "MC": "Monaco", + "MD": "Moldova", + "ME": "Montenegro", + "MF": "Saint Martin", + "MG": "Madagascar", + "MH": "Marshall Islands", + "MK": "North Macedonia", + "ML": "Mali", + "MM": "Myanmar", + "MN": "Mongolia", + "MO": "Macao", + "MP": "Northern Mariana Islands", + "MQ": "Martinique", + "MR": "Mauritania", + "MS": "Montserrat", + "MT": "Malta", + "MU": "Mauritius", + "MV": "Maldives", + "MW": "Malawi", + "MX": "Mexico", + "MY": "Malaysia", + "MZ": "Mozambique", + "NA": "Namibia", + "NC": "New Caledonia", + "NE": "Niger", + "NF": "Norfolk Island", + "NG": "Nigeria", + "NI": "Nicaragua", + "NL": "Netherlands", + "NO": "Norway", + "NP": "Nepal", + "NR": "Nauru", + "NU": "Niue", + "NZ": "New Zealand", + "OM": "Oman", + "PA": "Panama", + "PE": "Peru", + "PF": "French Polynesia", + "PG": "Papua New Guinea", + "PH": "Philippines", + "PK": "Pakistan", + "PL": "Poland", + "PM": "Saint Pierre and Miquelon", + "PN": "Pitcairn", + "PR": "Puerto Rico", + "PS": "Palestine", + "PT": "Portugal", + "PW": "Palau", + "PY": "Paraguay", + "QA": "Qatar", + "RE": "Reunion", + "RO": "Romania", + "RS": "Serbia", + "RU": "Russia", + "RW": "Rwanda", + "SA": "Saudi Arabia", + "SB": "Solomon Islands", + "SC": "Seychelles", + "SD": "Sudan", + "SE": "Sweden", + "SG": "Singapore", + "SH": "Saint Helena", + "SI": "Slovenia", + "SJ": "Svalbard and Jan Mayen", + "SK": "Slovakia", + "SL": "Sierra Leone", + "SM": "San Marino", + "SN": "Senegal", + "SO": "Somalia", + "SR": "Suriname", + "SS": "South Sudan", + "ST": "Sao Tome and Principe", + "SV": "El Salvador", + "SX": "Sint Maarten", + "SY": "Syria", + "SZ": "Eswatini", + "TC": "Turks and Caicos Islands", + "TD": "Chad", + "TF": "French Southern Territories", + "TG": "Togo", + "TH": "Thailand", + "TJ": "Tajikistan", + "TK": "Tokelau", + "TL": "Timor-Leste", + "TM": "Turkmenistan", + "TN": "Tunisia", + "TO": "Tonga", + "TR": "Turkey", + "TT": "Trinidad and Tobago", + "TV": "Tuvalu", + "TW": "Taiwan", + "TZ": "Tanzania", + "UA": "Ukraine", + "UG": "Uganda", + "UM": "United States Minor Outlying Islands", + "US": "United States", + "UY": "Uruguay", + "UZ": "Uzbekistan", + "VA": "Vatican City", + "VC": "Saint Vincent and the Grenadines", + "VE": "Venezuela", + "VG": "British Virgin Islands", + "VI": "U.S. Virgin Islands", + "VN": "Vietnam", + "VU": "Vanuatu", + "WF": "Wallis and Futuna", + "WS": "Samoa", + "YE": "Yemen", + "YT": "Mayotte", + "ZA": "South Africa", + "ZM": "Zambia", + "ZW": "Zimbabwe", +} + +// 主要城市到国家的映射表 +var cityToCountry = map[string]string{ + // China + "Beijing": "China", "Shanghai": "China", "Guangzhou": "China", "Shenzhen": "China", "HK": "China", "Tsuen Wan": "China", + "Chengdu": "China", "Hangzhou": "China", "Wuhan": "China", "Xi'an": "China", + "Nanjing": "China", "Tianjin": "China", "Chongqing": "China", "Shenyang": "China", + "Dalian": "China", "Qingdao": "China", "Jinan": "China", "Harbin": "China", + "Changchun": "China", "Shijiazhuang": "China", "Taiyuan": "China", "Hohhot": "China", + "Yinchuan": "China", "Lanzhou": "China", "Xining": "China", "Urumqi": "China", + "Lhasa": "China", "Kunming": "China", "Guiyang": "China", "Nanning": "China", + "Haikou": "China", "Changsha": "China", "Zhengzhou": "China", "Nanchang": "China", + "Hefei": "China", "Fuzhou": "China", "Taipei": "China", "Hong Kong": "China", "Macao": "China", + + // United States + "New York": "United States", "Chicago": "United States", + "Houston": "United States", "Phoenix": "United States", "Philadelphia": "United States", + "San Antonio": "United States", "San Diego": "United States", "Dallas": "United States", + "San Jose": "United States", "Austin": "United States", "Jacksonville": "United States", + "Fort Worth": "United States", "Columbus": "United States", "Indianapolis": "United States", + "Charlotte": "United States", "San Francisco": "United States", "Seattle": "United States", + "Denver": "United States", "Washington": "United States", "Boston": "United States", + "El Paso": "United States", "Detroit": "United States", "Nashville": "United States", + "Portland Oregon": "United States", "Memphis": "United States", "Oklahoma City": "United States", + "Las Vegas": "United States", "Louisville": "United States", "Baltimore": "United States", + "Milwaukee": "United States", "Albuquerque": "United States", "Tucson": "United States", + "Fresno": "United States", "Sacramento": "United States", "Mesa": "United States", + "Kansas City": "United States", "Atlanta": "United States", "Long Beach": "United States", + "Colorado Springs": "United States", "Raleigh": "United States", "Miami": "United States", + "Virginia Beach": "United States", "Omaha": "United States", "Oakland": "United States", + "Minneapolis": "United States", "Tulsa": "United States", "Arlington": "United States", + + // Japan + "Tokyo": "Japan", "Osaka": "Japan", "Yokohama": "Japan", "Nagoya": "Japan", + "Sapporo": "Japan", "Fukuoka": "Japan", "Kobe": "Japan", "Kawasaki": "Japan", + "Kyoto": "Japan", "Saitama": "Japan", "Hiroshima": "Japan", "Sendai": "Japan", + "Kitakyushu": "Japan", "Chiba": "Japan", "Sakai": "Japan", "Niigata": "Japan", + "Hamamatsu": "Japan", "Okayama": "Japan", "Sagamihara": "Japan", "Kumamoto": "Japan", + + // United Kingdom + "London": "United Kingdom", "Birmingham": "United Kingdom", "Manchester": "United Kingdom", + "Glasgow": "United Kingdom", "Liverpool": "United Kingdom", "Edinburgh": "United Kingdom", + "Leeds": "United Kingdom", "Sheffield": "United Kingdom", "Bristol": "United Kingdom", + "Cardiff": "United Kingdom", "Belfast": "United Kingdom", "Leicester": "United Kingdom", + "Coventry": "United Kingdom", "Bradford": "United Kingdom", "Nottingham": "United Kingdom", + "Kingston upon Hull": "United Kingdom", "Newcastle upon Tyne": "United Kingdom", + "Stoke-on-Trent": "United Kingdom", "Southampton": "United Kingdom", "Derby": "United Kingdom", + + // Germany + "Berlin": "Germany", "Hamburg": "Germany", "Munich": "Germany", "Cologne": "Germany", + "Frankfurt": "Germany", "Stuttgart": "Germany", "Düsseldorf": "Germany", "Dortmund": "Germany", + "Essen": "Germany", "Leipzig": "Germany", "Bremen": "Germany", "Dresden": "Germany", + "Hanover": "Germany", "Nuremberg": "Germany", "Duisburg": "Germany", "Bochum": "Germany", + "Wuppertal": "Germany", "Bielefeld": "Germany", "Bonn": "Germany", "Münster": "Germany", + + // France + "Paris": "France", "Marseille": "France", "Lyon": "France", "Toulouse": "France", + "Nice": "France", "Nantes": "France", "Strasbourg": "France", "Montpellier": "France", + "Bordeaux": "France", "Lille": "France", "Rennes": "France", "Reims": "France", + "Le Havre": "France", "Saint-Étienne": "France", "Toulon": "France", "Grenoble": "France", + "Dijon": "France", "Angers": "France", "Nîmes": "France", "Villeurbanne": "France", + + // Italy + "Rome": "Italy", "Milan": "Italy", "Naples": "Italy", "Turin": "Italy", + "Palermo": "Italy", "Genoa": "Italy", "Bologna": "Italy", "Florence": "Italy", + "Bari": "Italy", "Catania": "Italy", "Venice": "Italy", "Verona": "Italy", + "Messina": "Italy", "Padua": "Italy", "Trieste": "Italy", "Taranto": "Italy", + + // Spain + "Madrid": "Spain", "Seville": "Spain", + "Zaragoza": "Spain", "Málaga": "Spain", "Murcia": "Spain", "Palma": "Spain", + "Las Palmas": "Spain", "Bilbao": "Spain", "Alicante": "Spain", + "Valladolid": "Spain", "Vigo": "Spain", "Gijón": "Spain", "Hospitalet": "Spain", + "A Coruña": "Spain", "Vitoria-Gasteiz": "Spain", "Granada": "Spain", "Elche": "Spain", + + // Canada + "Toronto": "Canada", "Montreal": "Canada", "Vancouver": "Canada", "Calgary": "Canada", + "Edmonton": "Canada", "Ottawa": "Canada", "Winnipeg": "Canada", "Quebec City": "Canada", + "Hamilton Canada": "Canada", "Kitchener": "Canada", "London Ontario": "Canada", "Victoria Canada": "Canada", + "Halifax": "Canada", "Oshawa": "Canada", "Windsor Canada": "Canada", "Saskatoon": "Canada", + "Regina": "Canada", "St. John's": "Canada", "Kelowna": "Canada", "Barrie": "Canada", + + // Australia + "Sydney": "Australia", "Melbourne": "Australia", "Brisbane": "Australia", "Perth": "Australia", + "Adelaide": "Australia", "Gold Coast": "Australia", "Newcastle Australia": "Australia", "Canberra": "Australia", + "Sunshine Coast": "Australia", "Wollongong": "Australia", "Hobart": "Australia", "Geelong": "Australia", + "Townsville": "Australia", "Cairns": "Australia", "Toowoomba": "Australia", "Darwin": "Australia", + + // India + "Mumbai": "India", "Delhi": "India", "Bangalore": "India", "Hyderabad": "India", + "Ahmedabad": "India", "Chennai": "India", "Kolkata": "India", "Surat": "India", + "Pune": "India", "Jaipur": "India", "Lucknow": "India", "Kanpur": "India", + "Nagpur": "India", "Indore": "India", "Thane": "India", "Bhopal": "India", + "Visakhapatnam": "India", "Pimpri": "India", "Patna": "India", "Vadodara": "India", + "Ludhiana": "India", "Agra": "India", "Nashik": "India", "Faridabad": "India", + "Meerut": "India", "Rajkot": "India", "Kalyan": "India", "Vasai": "India", + + // Brazil + "São Paulo": "Brazil", "Rio de Janeiro": "Brazil", "Brasília": "Brazil", "Salvador": "Brazil", + "Fortaleza": "Brazil", "Belo Horizonte": "Brazil", "Manaus": "Brazil", "Curitiba": "Brazil", + "Recife": "Brazil", "Porto Alegre": "Brazil", "Belém": "Brazil", "Goiânia": "Brazil", + "Guarulhos": "Brazil", "Campinas": "Brazil", "São Luís": "Brazil", "São Gonçalo": "Brazil", + + // Russia + "Moscow": "Russia", "Saint Petersburg": "Russia", "Novosibirsk": "Russia", "Yekaterinburg": "Russia", + "Nizhny Novgorod": "Russia", "Kazan": "Russia", "Chelyabinsk": "Russia", "Omsk": "Russia", + "Samara": "Russia", "Rostov-on-Don": "Russia", "Ufa": "Russia", "Krasnoyarsk": "Russia", + "Perm": "Russia", "Voronezh": "Russia", "Volgograd": "Russia", "Krasnodar": "Russia", + + // South Korea + "Seoul": "South Korea", "Busan": "South Korea", "Incheon": "South Korea", "Daegu": "South Korea", + "Daejeon": "South Korea", "Gwangju": "South Korea", "Suwon": "South Korea", "Ulsan": "South Korea", + "Changwon": "South Korea", "Goyang": "South Korea", "Yongin": "South Korea", "Bucheon": "South Korea", + + // Mexico + "Mexico City": "Mexico", "Guadalajara": "Mexico", "Monterrey": "Mexico", "Puebla": "Mexico", + "Tijuana": "Mexico", "León": "Mexico", "Juárez": "Mexico", "Torreón": "Mexico", + "Querétaro": "Mexico", "San Luis Potosí": "Mexico", "Mexicali": "Mexico", + + // Indonesia + "Jakarta": "Indonesia", "Surabaya": "Indonesia", "Bandung": "Indonesia", "Bekasi": "Indonesia", + "Medan": "Indonesia", "Tangerang": "Indonesia", "Depok": "Indonesia", "Semarang": "Indonesia", + "Palembang": "Indonesia", "Makassar": "Indonesia", "Batam": "Indonesia", "Bogor": "Indonesia", + + // Turkey + "Istanbul": "Turkey", "Ankara": "Turkey", "Izmir": "Turkey", "Bursa": "Turkey", + "Adana": "Turkey", "Gaziantep": "Turkey", "Konya": "Turkey", "Antalya": "Turkey", + "Kayseri": "Turkey", "Mersin": "Turkey", "Eskişehir": "Turkey", "Diyarbakır": "Turkey", + + // Netherlands + "Amsterdam": "Netherlands", "Rotterdam": "Netherlands", "The Hague": "Netherlands", "Utrecht": "Netherlands", + "Eindhoven": "Netherlands", "Tilburg": "Netherlands", "Groningen": "Netherlands", "Almere": "Netherlands", + "Breda": "Netherlands", "Nijmegen": "Netherlands", "Enschede": "Netherlands", "Haarlem": "Netherlands", + + // Saudi Arabia + "Riyadh": "Saudi Arabia", "Jeddah": "Saudi Arabia", "Mecca": "Saudi Arabia", "Medina": "Saudi Arabia", + "Dammam": "Saudi Arabia", "Khobar": "Saudi Arabia", "Tabuk": "Saudi Arabia", "Buraidah": "Saudi Arabia", + "Khamis Mushait": "Saudi Arabia", "Hafar Al-Batin": "Saudi Arabia", "Jubail": "Saudi Arabia", "Taif": "Saudi Arabia", + + // Argentina + "Buenos Aires": "Argentina", "Rosario": "Argentina", "Mendoza": "Argentina", + "Tucumán": "Argentina", "La Plata": "Argentina", "Mar del Plata": "Argentina", "Salta": "Argentina", + "Santa Fe": "Argentina", "San Juan": "Argentina", "Resistencia": "Argentina", "Santiago del Estero": "Argentina", + + // Poland + "Warsaw": "Poland", "Kraków": "Poland", "Łódź": "Poland", "Wrocław": "Poland", + "Poznań": "Poland", "Gdańsk": "Poland", "Szczecin": "Poland", "Bydgoszcz": "Poland", + "Lublin": "Poland", "Katowice": "Poland", "Białystok": "Poland", "Gdynia": "Poland", + + // Ukraine + "Kiev": "Ukraine", "Kharkiv": "Ukraine", "Odessa": "Ukraine", "Dnipro": "Ukraine", + "Donetsk": "Ukraine", "Zaporizhzhia": "Ukraine", "Lviv": "Ukraine", "Kryvyi Rih": "Ukraine", + "Mykolaiv": "Ukraine", "Mariupol": "Ukraine", "Luhansk": "Ukraine", "Vinnytsia": "Ukraine", + + // Egypt + "Cairo": "Egypt", "Alexandria": "Egypt", "Giza": "Egypt", "Shubra El Kheima": "Egypt", + "Port Said": "Egypt", "Suez": "Egypt", "Luxor": "Egypt", "Mansoura": "Egypt", + "El Mahalla El Kubra": "Egypt", "Tanta": "Egypt", "Asyut": "Egypt", "Ismailia": "Egypt", + + // Nigeria + "Lagos": "Nigeria", "Kano": "Nigeria", "Ibadan": "Nigeria", "Abuja": "Nigeria", + "Port Harcourt": "Nigeria", "Benin City": "Nigeria", "Maiduguri": "Nigeria", "Zaria": "Nigeria", + "Aba": "Nigeria", "Jos": "Nigeria", "Ilorin": "Nigeria", "Oyo": "Nigeria", + + // South Africa + "Johannesburg": "South Africa", "Cape Town": "South Africa", "Durban": "South Africa", "Pretoria": "South Africa", + "Soweto": "South Africa", "Port Elizabeth": "South Africa", "Pietermaritzburg": "South Africa", "Benoni": "South Africa", + "Tembisa": "South Africa", "East London": "South Africa", "Vereeniging": "South Africa", "Bloemfontein": "South Africa", + + // Iran + "Tehran": "Iran", "Mashhad": "Iran", "Isfahan": "Iran", "Karaj": "Iran", + "Shiraz": "Iran", "Tabriz": "Iran", "Qom": "Iran", "Ahvaz": "Iran", + "Kermanshah": "Iran", "Urmia": "Iran", "Rasht": "Iran", "Zahedan": "Iran", + + // Thailand + "Bangkok": "Thailand", "Nonthaburi": "Thailand", "Pak Kret": "Thailand", "Hat Yai": "Thailand", + "Chiang Mai": "Thailand", "Phuket": "Thailand", "Pattaya": "Thailand", "Nakhon Ratchasima": "Thailand", + "Khon Kaen": "Thailand", "Udon Thani": "Thailand", "Surat Thani": "Thailand", "Nakhon Si Thammarat": "Thailand", + + // Vietnam + "Ho Chi Minh City": "Vietnam", "Hanoi": "Vietnam", "Haiphong": "Vietnam", "Da Nang": "Vietnam", + "Bien Hoa": "Vietnam", "Hue": "Vietnam", "Nha Trang": "Vietnam", "Can Tho": "Vietnam", + "Rach Gia": "Vietnam", "Qui Nhon": "Vietnam", "Vung Tau": "Vietnam", "Nam Dinh": "Vietnam", + + // Philippines + "Manila": "Philippines", "Quezon City": "Philippines", "Davao": "Philippines", "Caloocan": "Philippines", + "Cebu City": "Philippines", "Zamboanga": "Philippines", "Antipolo": "Philippines", "Taguig": "Philippines", + "Pasig": "Philippines", "Cagayan de Oro": "Philippines", "Paranaque": "Philippines", "Makati": "Philippines", + + // Malaysia + "Kuala Lumpur": "Malaysia", "George Town": "Malaysia", "Ipoh": "Malaysia", "Shah Alam": "Malaysia", + "Petaling Jaya": "Malaysia", "Johor Bahru": "Malaysia", "Seremban": "Malaysia", "Kuching": "Malaysia", + "Kota Kinabalu": "Malaysia", "Klang": "Malaysia", "Kajang": "Malaysia", "Subang Jaya": "Malaysia", + + // Bangladesh + "Dhaka": "Bangladesh", "Chittagong": "Bangladesh", "Sylhet": "Bangladesh", "Khulna": "Bangladesh", + "Rajshahi": "Bangladesh", "Rangpur": "Bangladesh", "Barisal": "Bangladesh", "Comilla": "Bangladesh", + "Mymensingh": "Bangladesh", "Narayanganj": "Bangladesh", "Gazipur": "Bangladesh", "Tongi": "Bangladesh", + + // Pakistan + "Karachi": "Pakistan", "Lahore": "Pakistan", "Faisalabad": "Pakistan", "Rawalpindi": "Pakistan", + "Gujranwala": "Pakistan", "Peshawar": "Pakistan", "Multan": "Pakistan", "Islamabad": "Pakistan", + "Quetta": "Pakistan", "Bahawalpur": "Pakistan", "Sargodha": "Pakistan", "Sialkot": "Pakistan", + + // Afghanistan + "Kabul": "Afghanistan", "Kandahar": "Afghanistan", "Herat": "Afghanistan", "Mazar-i-Sharif": "Afghanistan", + "Jalalabad": "Afghanistan", "Kunduz": "Afghanistan", "Ghazni": "Afghanistan", "Bamyan": "Afghanistan", + + // Israel + "Jerusalem": "Israel", "Tel Aviv": "Israel", "Haifa": "Israel", "Rishon LeZion": "Israel", + "Petah Tikva": "Israel", "Ashdod": "Israel", "Netanya": "Israel", "Beer Sheva": "Israel", + "Holon": "Israel", "Bnei Brak": "Israel", "Ramat Gan": "Israel", "Ashkelon": "Israel", + + // Iraq + "Baghdad": "Iraq", "Basra": "Iraq", "Mosul": "Iraq", "Erbil": "Iraq", + "Sulaymaniyah": "Iraq", "Najaf": "Iraq", "Karbala": "Iraq", "Nasiriyah": "Iraq", + "Amarah": "Iraq", "Duhok": "Iraq", "Ramadi": "Iraq", "Fallujah": "Iraq", + + // Morocco + "Casablanca": "Morocco", "Rabat": "Morocco", "Fes": "Morocco", "Marrakech": "Morocco", + "Tangier": "Morocco", "Meknes": "Morocco", "Oujda": "Morocco", "Kenitra": "Morocco", + "Tetouan": "Morocco", "Safi": "Morocco", "El Jadida": "Morocco", "Nador": "Morocco", + + // Algeria + "Algiers": "Algeria", "Oran": "Algeria", "Constantine": "Algeria", "Batna": "Algeria", + "Djelfa": "Algeria", "Setif": "Algeria", "Annaba": "Algeria", "Sidi Bel Abbes": "Algeria", + "Biskra": "Algeria", "Tebessa": "Algeria", "El Oued": "Algeria", "Skikda": "Algeria", + + // Kenya + "Nairobi": "Kenya", "Mombasa": "Kenya", "Kisumu": "Kenya", "Nakuru": "Kenya", + "Eldoret": "Kenya", "Kitale": "Kenya", "Malindi": "Kenya", "Garissa": "Kenya", + "Kakamega": "Kenya", "Nyeri": "Kenya", "Machakos": "Kenya", "Meru": "Kenya", + + // Ethiopia + "Addis Ababa": "Ethiopia", "Dire Dawa": "Ethiopia", "Mek'ele": "Ethiopia", "Gondar": "Ethiopia", + "Adama": "Ethiopia", "Awasa": "Ethiopia", "Bahir Dar": "Ethiopia", "Dessie": "Ethiopia", + "Jimma": "Ethiopia", "Jijiga": "Ethiopia", "Shashamane": "Ethiopia", "Nekemte": "Ethiopia", + + // Ghana + "Accra": "Ghana", "Kumasi": "Ghana", "Tamale": "Ghana", "Sekondi-Takoradi": "Ghana", + "Ashaiman": "Ghana", "Cape Coast": "Ghana", "Obuasi": "Ghana", "Teshie": "Ghana", + "Madina": "Ghana", "Koforidua": "Ghana", "Wa": "Ghana", "Techiman": "Ghana", + + // Chile + "Santiago": "Chile", "Valparaíso": "Chile", "Concepción": "Chile", "La Serena": "Chile", + "Antofagasta": "Chile", "Temuco": "Chile", "Rancagua": "Chile", "Talca": "Chile", + "Arica": "Chile", "Chillán": "Chile", "Iquique": "Chile", + + // Colombia + "Bogotá": "Colombia", "Medellín": "Colombia", "Cali": "Colombia", "Barranquilla": "Colombia", + "Cartagena": "Colombia", "Cúcuta": "Colombia", "Bucaramanga": "Colombia", "Pereira": "Colombia", + "Santa Marta": "Colombia", "Ibagué": "Colombia", "Pasto": "Colombia", "Manizales": "Colombia", + + // Peru + "Lima": "Peru", "Arequipa": "Peru", "Trujillo": "Peru", "Chiclayo": "Peru", + "Piura": "Peru", "Iquitos": "Peru", "Cusco": "Peru", "Chimbote": "Peru", + "Huancayo": "Peru", "Tacna": "Peru", "Juliaca": "Peru", "Ica": "Peru", + + // Venezuela + "Caracas": "Venezuela", "Maracaibo": "Venezuela", "Barquisimeto": "Venezuela", + "Maracay": "Venezuela", "Ciudad Guayana": "Venezuela", "San Cristóbal": "Venezuela", "Maturín": "Venezuela", + "Ciudad Bolívar": "Venezuela", "Cumana": "Venezuela", + + // Ecuador + "Guayaquil": "Ecuador", "Quito": "Ecuador", "Cuenca": "Ecuador", "Santo Domingo": "Ecuador", + "Machala": "Ecuador", "Durán": "Ecuador", "Manta": "Ecuador", "Portoviejo": "Ecuador", + "Ambato": "Ecuador", "Riobamba": "Ecuador", "Loja": "Ecuador", "Esmeraldas": "Ecuador", + + // Bolivia + "Santa Cruz": "Bolivia", "La Paz": "Bolivia", "Cochabamba": "Bolivia", "Oruro": "Bolivia", + "Sucre": "Bolivia", "Tarija": "Bolivia", "Potosí": "Bolivia", "Trinidad": "Bolivia", + + // Uruguay + "Montevideo": "Uruguay", "Salto": "Uruguay", "Paysandú": "Uruguay", "Las Piedras": "Uruguay", + "Rivera": "Uruguay", "Maldonado": "Uruguay", "Tacuarembó": "Uruguay", "Melo": "Uruguay", + + // Paraguay + "Asunción": "Paraguay", "Ciudad del Este": "Paraguay", "San Lorenzo": "Paraguay", "Luque": "Paraguay", + "Capiatá": "Paraguay", "Lambaré": "Paraguay", "Fernando de la Mora": "Paraguay", "Limpio": "Paraguay", + + // Norway + "Oslo": "Norway", "Bergen": "Norway", "Trondheim": "Norway", "Stavanger": "Norway", + "Bærum": "Norway", "Kristiansand": "Norway", "Fredrikstad": "Norway", "Sandnes": "Norway", + "Tromsø": "Norway", "Drammen": "Norway", "Asker": "Norway", "Lillestrøm": "Norway", + + // Sweden + "Stockholm": "Sweden", "Gothenburg": "Sweden", "Malmö": "Sweden", "Uppsala": "Sweden", + "Västerås": "Sweden", "Örebro": "Sweden", "Linköping": "Sweden", "Helsingborg": "Sweden", + "Jönköping": "Sweden", "Norrköping": "Sweden", "Lund": "Sweden", "Umeå": "Sweden", + + // Finland + "Helsinki": "Finland", "Espoo": "Finland", "Tampere": "Finland", "Vantaa": "Finland", + "Oulu": "Finland", "Turku": "Finland", "Jyväskylä": "Finland", "Lahti": "Finland", + "Kuopio": "Finland", "Pori": "Finland", "Joensuu": "Finland", "Lappeenranta": "Finland", + + // Denmark + "Copenhagen": "Denmark", "Aarhus": "Denmark", "Odense": "Denmark", "Aalborg": "Denmark", + "Esbjerg": "Denmark", "Randers": "Denmark", "Kolding": "Denmark", "Horsens": "Denmark", + "Vejle": "Denmark", "Roskilde": "Denmark", "Herning": "Denmark", "Silkeborg": "Denmark", + + // Switzerland + "Zurich": "Switzerland", "Geneva": "Switzerland", "Basel": "Switzerland", "Bern": "Switzerland", + "Lausanne": "Switzerland", "Winterthur": "Switzerland", "Lucerne": "Switzerland", "St. Gallen": "Switzerland", + "Lugano": "Switzerland", "Biel": "Switzerland", "Thun": "Switzerland", "Köniz": "Switzerland", + + // Austria + "Innsbruck": "Austria", "Klagenfurt": "Austria", "Villach": "Austria", "Wels": "Austria", + "Sankt Pölten": "Austria", "Dornbirn": "Austria", "Steyr": "Austria", "Wiener Neustadt": "Austria", + "Feldkirch": "Austria", "Bregenz": "Austria", "Leonding": "Austria", "Klosterneuburg": "Austria", + + // Belgium + "Brussels": "Belgium", "Antwerp": "Belgium", "Ghent": "Belgium", "Charleroi": "Belgium", + "Liège": "Belgium", "Bruges": "Belgium", "Namur": "Belgium", "Leuven": "Belgium", + "Mons": "Belgium", "Aalst": "Belgium", "Mechelen": "Belgium", "La Louvière": "Belgium", + + // Czech Republic (Czechia) + "Prague": "Czechia", "Brno": "Czechia", "Ostrava": "Czechia", "Plzen": "Czechia", + "Liberec": "Czechia", "Olomouc": "Czechia", "Budweis": "Czechia", "Hradec Králové": "Czechia", + "Ústí nad Labem": "Czechia", "Pardubice": "Czechia", "Zlín": "Czechia", "Havířov": "Czechia", + + // Hungary + "Budapest": "Hungary", "Debrecen": "Hungary", "Szeged": "Hungary", "Miskolc": "Hungary", + "Pécs": "Hungary", "Győr": "Hungary", "Nyíregyháza": "Hungary", "Kecskemét": "Hungary", + "Székesfehérvár": "Hungary", "Szombathely": "Hungary", "Érd": "Hungary", "Tatabánya": "Hungary", + + // Romania + "Bucharest": "Romania", "Cluj-Napoca": "Romania", "Timișoara": "Romania", "Iași": "Romania", + "Constanța": "Romania", "Craiova": "Romania", "Brașov": "Romania", "Galați": "Romania", + "Ploiești": "Romania", "Oradea": "Romania", "Brăila": "Romania", "Arad": "Romania", + + // Serbia + "Belgrade": "Serbia", "Novi Sad": "Serbia", "Niš": "Serbia", "Kragujevac": "Serbia", + "Subotica": "Serbia", "Zrenjanin": "Serbia", "Pančevo": "Serbia", "Čačak": "Serbia", + "Novi Pazar": "Serbia", "Kraljevo": "Serbia", "Smederevo": "Serbia", "Leskovac": "Serbia", + + // Croatia + "Zagreb": "Croatia", "Split": "Croatia", "Rijeka": "Croatia", "Osijek": "Croatia", + "Zadar": "Croatia", "Pula": "Croatia", "Slavonski Brod": "Croatia", "Karlovac": "Croatia", + "Varaždin": "Croatia", "Šibenik": "Croatia", "Sisak": "Croatia", "Velika Gorica": "Croatia", + + // Greece + "Athens": "Greece", "Thessaloniki": "Greece", "Patras": "Greece", "Heraklion": "Greece", + "Larissa": "Greece", "Volos": "Greece", "Rhodes": "Greece", "Ioannina": "Greece", + "Chania": "Greece", "Chalcis": "Greece", "Serres": "Greece", "Alexandroupoli": "Greece", + + // Portugal + "Lisbon": "Portugal", "Porto": "Portugal", "Vila Nova de Gaia": "Portugal", "Amadora": "Portugal", + "Braga": "Portugal", "Funchal": "Portugal", "Coimbra": "Portugal", "Setúbal": "Portugal", + "Almada": "Portugal", "Agualva-Cacém": "Portugal", "Queluz": "Portugal", "Rio Tinto": "Portugal", + + // Bulgaria + "Sofia": "Bulgaria", "Plovdiv": "Bulgaria", "Varna": "Bulgaria", "Burgas": "Bulgaria", + "Ruse": "Bulgaria", "Stara Zagora": "Bulgaria", "Pleven": "Bulgaria", "Sliven": "Bulgaria", + "Dobrich": "Bulgaria", "Shumen": "Bulgaria", "Pernik": "Bulgaria", "Haskovo": "Bulgaria", + + // Slovakia + "Bratislava": "Slovakia", "Košice": "Slovakia", "Prešov": "Slovakia", "Žilina": "Slovakia", + "Banská Bystrica": "Slovakia", "Nitra": "Slovakia", "Trnava": "Slovakia", "Martin": "Slovakia", + "Trenčín": "Slovakia", "Poprad": "Slovakia", "Prievidza": "Slovakia", "Zvolen": "Slovakia", + + // Slovenia + "Ljubljana": "Slovenia", "Maribor": "Slovenia", "Celje": "Slovenia", "Kranj": "Slovenia", + "Velenje": "Slovenia", "Koper": "Slovenia", "Novo Mesto": "Slovenia", "Ptuj": "Slovenia", + "Trbovlje": "Slovenia", "Kamnik": "Slovenia", "Jesenice": "Slovenia", "Nova Gorica": "Slovenia", + + // Lithuania + "Vilnius": "Lithuania", "Kaunas": "Lithuania", "Klaipėda": "Lithuania", "Šiauliai": "Lithuania", + "Panevėžys": "Lithuania", "Alytus": "Lithuania", "Marijampolė": "Lithuania", "Mažeikiai": "Lithuania", + "Jonava": "Lithuania", "Utena": "Lithuania", "Kėdainiai": "Lithuania", "Telšiai": "Lithuania", + + // Latvia + "Riga": "Latvia", "Daugavpils": "Latvia", "Liepāja": "Latvia", "Jelgava": "Latvia", + "Jūrmala": "Latvia", "Ventspils": "Latvia", "Rēzekne": "Latvia", "Valmiera": "Latvia", + "Jēkabpils": "Latvia", "Ogre": "Latvia", "Tukums": "Latvia", "Salaspils": "Latvia", + + // Estonia + "Tallinn": "Estonia", "Tartu": "Estonia", "Narva": "Estonia", "Pärnu": "Estonia", + "Kohtla-Järve": "Estonia", "Viljandi": "Estonia", "Rakvere": "Estonia", "Maardu": "Estonia", + "Sillamäe": "Estonia", "Kuressaare": "Estonia", "Võru": "Estonia", "Valga": "Estonia", + + // New Zealand + "Auckland": "New Zealand", "Wellington": "New Zealand", "Christchurch": "New Zealand", "Hamilton": "New Zealand", + "Tauranga": "New Zealand", "Napier-Hastings": "New Zealand", "Dunedin": "New Zealand", "Palmerston North": "New Zealand", + "Nelson": "New Zealand", "Rotorua": "New Zealand", "New Plymouth": "New Zealand", "Whangarei": "New Zealand", + + // Singapore + "Singapore": "Singapore", + + // United Arab Emirates + "Dubai": "United Arab Emirates", "Abu Dhabi": "United Arab Emirates", "Sharjah": "United Arab Emirates", "Al Ain": "United Arab Emirates", + "Ajman": "United Arab Emirates", "Ras Al Khaimah": "United Arab Emirates", "Fujairah": "United Arab Emirates", "Umm Al Quwain": "United Arab Emirates", + + // Lebanon + "Beirut": "Lebanon", "Sidon": "Lebanon", "Tyre": "Lebanon", + "Nabatieh": "Lebanon", "Jounieh": "Lebanon", "Zahle": "Lebanon", "Baalbek": "Lebanon", + + // Jordan + "Amman": "Jordan", "Zarqa": "Jordan", "Irbid": "Jordan", "Russeifa": "Jordan", + "Wadi as-Sir": "Jordan", "Aqaba": "Jordan", "Madaba": "Jordan", "Salt": "Jordan", + + // Yemen + "Sanaa": "Yemen", "Aden": "Yemen", "Taiz": "Yemen", "Hodeidah": "Yemen", + "Mukalla": "Yemen", "Ibb": "Yemen", "Dhamar": "Yemen", "Zinjibar": "Yemen", + + // Syria + "Damascus": "Syria", "Aleppo": "Syria", "Homs": "Syria", "Latakia": "Syria", + "Hama": "Syria", "Raqqa": "Syria", "Deir ez-Zor": "Syria", "Hasakah": "Syria", + + // Oman + "Muscat": "Oman", "Seeb": "Oman", "Salalah": "Oman", "Bawshar": "Oman", + "Sohar": "Oman", "Sur": "Oman", "Ibra": "Oman", "Nizwa": "Oman", + + // Qatar + "Doha": "Qatar", "Al Rayyan": "Qatar", "Umm Salal": "Qatar", "Al Wakrah": "Qatar", + "Al Khor": "Qatar", "Dukhan": "Qatar", "Lusail": "Qatar", "Mesaieed": "Qatar", + + // Kuwait + "Kuwait City": "Kuwait", "Al Ahmadi": "Kuwait", "Hawally": "Kuwait", "As Salimiyah": "Kuwait", + "Sabah as-Salim": "Kuwait", "Al Farwaniyah": "Kuwait", "Al Fahahil": "Kuwait", "Ar Riqqah": "Kuwait", + + // Bahrain + "Manama": "Bahrain", "Riffa": "Bahrain", "Muharraq": "Bahrain", "Hamad Town": "Bahrain", + "A'ali": "Bahrain", "Isa Town": "Bahrain", "Sitra": "Bahrain", "Budaiya": "Bahrain", + + // Cyprus + "Nicosia": "Cyprus", "Limassol": "Cyprus", "Larnaca": "Cyprus", "Famagusta": "Cyprus", + "Paphos": "Cyprus", "Kyrenia": "Cyprus", "Protaras": "Cyprus", "Paralimni": "Cyprus", + + // Malta + "Valletta": "Malta", "Birkirkara": "Malta", "Mosta": "Malta", "Qormi": "Malta", + "Zabbar": "Malta", "San Pawl il-Bahar": "Malta", "Tarxien": "Malta", "Naxxar": "Malta", + + // Iceland + "Reykjavik": "Iceland", "Kopavogur": "Iceland", "Hafnarfjordur": "Iceland", "Akureyri": "Iceland", + "Reykjanesbaer": "Iceland", "Gardabaer": "Iceland", "Mosfellsbaer": "Iceland", "Arborg": "Iceland", + + // Luxembourg + "Luxembourg City": "Luxembourg", "Esch-sur-Alzette": "Luxembourg", "Dudelange": "Luxembourg", "Schifflange": "Luxembourg", + "Bettembourg": "Luxembourg", "Petange": "Luxembourg", "Ettelbruck": "Luxembourg", "Diekirch": "Luxembourg", + + // Belarus + "Minsk": "Belarus", "Gomel": "Belarus", "Mogilev": "Belarus", "Vitebsk": "Belarus", + "Grodno": "Belarus", "Brest": "Belarus", "Bobruisk": "Belarus", "Baranovichi": "Belarus", + + // Moldova + "Chisinau": "Moldova", "Tiraspol": "Moldova", "Balti": "Moldova", "Bender": "Moldova", + "Rybnitsa": "Moldova", "Cahul": "Moldova", "Ungheni": "Moldova", "Soroca": "Moldova", + + // North Macedonia + "Skopje": "North Macedonia", "Bitola": "North Macedonia", "Kumanovo": "North Macedonia", "Prilep": "North Macedonia", + "Tetovo": "North Macedonia", "Veles": "North Macedonia", "Shtip": "North Macedonia", "Ohrid": "North Macedonia", + + // Albania + "Tirana": "Albania", "Durres": "Albania", "Vlore": "Albania", "Elbasan": "Albania", + "Shkoder": "Albania", "Fier": "Albania", "Korce": "Albania", "Berat": "Albania", + + // Montenegro + "Podgorica": "Montenegro", "Niksic": "Montenegro", "Pljevlja": "Montenegro", "Bijelo Polje": "Montenegro", + "Cetinje": "Montenegro", "Bar": "Montenegro", "Herceg Novi": "Montenegro", "Berane": "Montenegro", + + // Bosnia and Herzegovina + "Sarajevo": "Bosnia and Herzegovina", "Banja Luka": "Bosnia and Herzegovina", "Tuzla": "Bosnia and Herzegovina", "Zenica": "Bosnia and Herzegovina", + "Mostar": "Bosnia and Herzegovina", "Prijedor": "Bosnia and Herzegovina", "Brčko": "Bosnia and Herzegovina", "Bijeljina": "Bosnia and Herzegovina", + + // Armenia + "Yerevan": "Armenia", "Gyumri": "Armenia", "Vanadzor": "Armenia", "Vagharshapat": "Armenia", + "Hrazdan": "Armenia", "Abovyan": "Armenia", "Kapan": "Armenia", "Armavir": "Armenia", + + // Georgia + "Tbilisi": "Georgia", "Kutaisi": "Georgia", "Batumi": "Georgia", "Rustavi": "Georgia", + "Gori": "Georgia", "Zugdidi": "Georgia", "Poti": "Georgia", "Kobuleti": "Georgia", + + // Azerbaijan + "Baku": "Azerbaijan", "Ganja": "Azerbaijan", "Sumqayit": "Azerbaijan", "Mingachevir": "Azerbaijan", + "Quba": "Azerbaijan", "Lankaran": "Azerbaijan", "Shaki": "Azerbaijan", "Yevlax": "Azerbaijan", + + // Kazakhstan + "Almaty": "Kazakhstan", "Nur-Sultan": "Kazakhstan", "Shymkent": "Kazakhstan", "Aktobe": "Kazakhstan", + "Taraz": "Kazakhstan", "Pavlodar": "Kazakhstan", "Ust-Kamenogorsk": "Kazakhstan", "Semey": "Kazakhstan", + "Atyrau": "Kazakhstan", "Kostanay": "Kazakhstan", "Kyzylorda": "Kazakhstan", "Oral": "Kazakhstan", + + // Uzbekistan + "Tashkent": "Uzbekistan", "Namangan": "Uzbekistan", "Samarkand": "Uzbekistan", "Andijan": "Uzbekistan", + "Nukus": "Uzbekistan", "Fergana": "Uzbekistan", "Bukhara": "Uzbekistan", "Qarshi": "Uzbekistan", + "Kokand": "Uzbekistan", "Margilan": "Uzbekistan", "Chirchiq": "Uzbekistan", "Termez": "Uzbekistan", + + // Kyrgyzstan + "Bishkek": "Kyrgyzstan", "Osh": "Kyrgyzstan", "Jalal-Abad": "Kyrgyzstan", "Karakol": "Kyrgyzstan", + "Tokmok": "Kyrgyzstan", "Uzgen": "Kyrgyzstan", "Naryn": "Kyrgyzstan", "Talas": "Kyrgyzstan", + + // Tajikistan + "Dushanbe": "Tajikistan", "Khujand": "Tajikistan", "Kulob": "Tajikistan", "Qurghonteppa": "Tajikistan", + "Istaravshan": "Tajikistan", "Isfara": "Tajikistan", "Panjakent": "Tajikistan", "Tursunzoda": "Tajikistan", + + // Turkmenistan + "Ashgabat": "Turkmenistan", "Turkmenbashi": "Turkmenistan", "Dasoguz": "Turkmenistan", "Mary": "Turkmenistan", + "Balkanabat": "Turkmenistan", "Bayramaly": "Turkmenistan", "Tejen": "Turkmenistan", "Serdar": "Turkmenistan", + + // Mongolia + "Ulaanbaatar": "Mongolia", "Erdenet": "Mongolia", "Darkhan": "Mongolia", "Choibalsan": "Mongolia", + "Murun": "Mongolia", "Bayankhongor": "Mongolia", "Ulgii": "Mongolia", "Khovd": "Mongolia", + + // Myanmar + "Yangon": "Myanmar", "Mandalay": "Myanmar", "Naypyidaw": "Myanmar", "Mawlamyine": "Myanmar", + "Bago": "Myanmar", "Pathein": "Myanmar", "Monywa": "Myanmar", "Meiktila": "Myanmar", + "Sittwe": "Myanmar", "Myitkyina": "Myanmar", "Dawei": "Myanmar", "Pyay": "Myanmar", + + // Laos + "Vientiane": "Laos", "Pakse": "Laos", "Savannakhet": "Laos", "Luang Prabang": "Laos", + "Thakhek": "Laos", "Muang Xay": "Laos", "Phonsavan": "Laos", "Muang Pakbeng": "Laos", + + // Cambodia + "Phnom Penh": "Cambodia", "Siem Reap": "Cambodia", "Battambang": "Cambodia", "Sihanoukville": "Cambodia", + "Poipet": "Cambodia", "Kampong Cham": "Cambodia", "Pursat": "Cambodia", "Kampong Speu": "Cambodia", + + // Sri Lanka + "Colombo": "Sri Lanka", "Dehiwala-Mount Lavinia": "Sri Lanka", "Moratuwa": "Sri Lanka", "Negombo": "Sri Lanka", + "Kandy": "Sri Lanka", "Kalmunai": "Sri Lanka", "Galle": "Sri Lanka", "Trincomalee": "Sri Lanka", + "Batticaloa": "Sri Lanka", "Jaffna": "Sri Lanka", "Katunayake": "Sri Lanka", "Dambulla": "Sri Lanka", + + // Nepal + "Kathmandu": "Nepal", "Pokhara": "Nepal", "Lalitpur": "Nepal", "Bharatpur": "Nepal", + "Biratnagar": "Nepal", "Birgunj": "Nepal", "Dharan": "Nepal", "Bhim Datta": "Nepal", + "Butwal": "Nepal", "Hetauda": "Nepal", "Dhangadhi": "Nepal", "Itahari": "Nepal", + + // Bhutan + "Thimphu": "Bhutan", "Phuntsholing": "Bhutan", "Punakha": "Bhutan", "Wangdue": "Bhutan", + "Samdrup Jongkhar": "Bhutan", "Gelephu": "Bhutan", "Trongsa": "Bhutan", "Mongar": "Bhutan", + + // Maldives + "Male": "Maldives", "Addu City": "Maldives", "Fuvahmulah": "Maldives", "Kulhudhuffushi": "Maldives", + "Thinadhoo": "Maldives", "Ungoofaaru": "Maldives", "Naifaru": "Maldives", "Dhidhdhoo": "Maldives", + + // Madagascar + "Antananarivo": "Madagascar", "Toamasina": "Madagascar", "Antsirabe": "Madagascar", "Fianarantsoa": "Madagascar", + "Mahajanga": "Madagascar", "Toliara": "Madagascar", "Antsiranana": "Madagascar", "Ambovombe": "Madagascar", + "Morondava": "Madagascar", "Sambava": "Madagascar", "Manakara": "Madagascar", "Farafangana": "Madagascar", + + // Mauritius + "Port Louis": "Mauritius", "Beau Bassin-Rose Hill": "Mauritius", "Vacoas-Phoenix": "Mauritius", "Curepipe": "Mauritius", + "Quatre Bornes": "Mauritius", "Triolet": "Mauritius", "Goodlands": "Mauritius", "Centre de Flacq": "Mauritius", + + // Seychelles + "Victoria": "Seychelles", "Anse Boileau": "Seychelles", "Beau Vallon": "Seychelles", "Cascade": "Seychelles", + "Anse Royale": "Seychelles", "Takamaka": "Seychelles", "Port Glaud": "Seychelles", "Grand Anse Mahe": "Seychelles", + + // Comoros + "Moroni": "Comoros", "Mutsamudu": "Comoros", "Fomboni": "Comoros", "Domoni": "Comoros", + "Sima": "Comoros", "Mitsoudje": "Comoros", "Adda-Doueni": "Comoros", "Ouani": "Comoros", + + // Djibouti + "Djibouti City": "Djibouti", "Ali Sabieh": "Djibouti", "Dikhil": "Djibouti", "Tadjoura": "Djibouti", + "Obock": "Djibouti", "Arta": "Djibouti", "Holhol": "Djibouti", "Yoboki": "Djibouti", + + // Eritrea + "Asmara": "Eritrea", "Assab": "Eritrea", "Massawa": "Eritrea", "Keren": "Eritrea", + "Mendefera": "Eritrea", "Barentu": "Eritrea", "Adi Keih": "Eritrea", "Adi Quala": "Eritrea", + + // Somalia + "Mogadishu": "Somalia", "Hargeisa": "Somalia", "Bosaso": "Somalia", "Kismayo": "Somalia", + "Merca": "Somalia", "Galcayo": "Somalia", "Berbera": "Somalia", "Baidoa": "Somalia", + "Garowe": "Somalia", "Jowhar": "Somalia", "Borama": "Somalia", "Las Anod": "Somalia", + + // Sudan + "Khartoum": "Sudan", "Omdurman": "Sudan", "Port Sudan": "Sudan", "Kassala": "Sudan", + "Obeid": "Sudan", "Nyala": "Sudan", "Gedaref": "Sudan", "Wad Medani": "Sudan", + "El Fasher": "Sudan", "Kosti": "Sudan", "Sennar": "Sudan", "Dongola": "Sudan", + + // South Sudan + "Juba": "South Sudan", "Malakal": "South Sudan", "Wau": "South Sudan", "Bentiu": "South Sudan", + "Yei": "South Sudan", "Aweil": "South Sudan", "Kuacjok": "South Sudan", "Bor": "South Sudan", + + // Chad + "N'Djamena": "Chad", "Moundou": "Chad", "Sarh": "Chad", "Abéché": "Chad", + "Kelo": "Chad", "Koumra": "Chad", "Pala": "Chad", "Am Timan": "Chad", + + // Central African Republic + "Bangui": "Central African Republic", "Bimbo": "Central African Republic", "Berbérati": "Central African Republic", "Carnot": "Central African Republic", + "Bambari": "Central African Republic", "Bouar": "Central African Republic", "Bossangoa": "Central African Republic", "Bria": "Central African Republic", + + // Democratic Republic of Congo + "Kinshasa": "Congo", "Lubumbashi": "Congo", "Mbuji-Mayi": "Congo", "Kisangani": "Congo", + "Masina": "Congo", "Kananga": "Congo", "Likasi": "Congo", "Kolwezi": "Congo", + "Tshikapa": "Congo", "Beni": "Congo", "Bukavu": "Congo", "Mwene-Ditu": "Congo", + + // Republic of Congo + "Brazzaville": "Congo", "Pointe-Noire": "Congo", "Dolisie": "Congo", "Nkayi": "Congo", + "Impfondo": "Congo", "Ouesso": "Congo", "Madingou": "Congo", "Owando": "Congo", + + // Gabon + "Libreville": "Gabon", "Port-Gentil": "Gabon", "Franceville": "Gabon", "Oyem": "Gabon", + "Moanda": "Gabon", "Mouila": "Gabon", "Lambaréné": "Gabon", "Tchibanga": "Gabon", + + // Equatorial Guinea + "Malabo": "Equatorial Guinea", "Bata": "Equatorial Guinea", "Ebebiyin": "Equatorial Guinea", "Aconibe": "Equatorial Guinea", + "Añisoc": "Equatorial Guinea", "Luba": "Equatorial Guinea", "Evinayong": "Equatorial Guinea", "Mengomeyén": "Equatorial Guinea", + + // Cameroon + "Yaoundé": "Cameroon", "Douala": "Cameroon", "Bamenda": "Cameroon", "Bafoussam": "Cameroon", + "Garoua": "Cameroon", "Maroua": "Cameroon", "Nkongsamba": "Cameroon", "Bertoua": "Cameroon", + "Edéa": "Cameroon", "Loum": "Cameroon", "Kumba": "Cameroon", "Foumban": "Cameroon", + + // Angola + "Luanda": "Angola", "Huambo": "Angola", "Lobito": "Angola", "Benguela": "Angola", + "Kuito": "Angola", "Lubango": "Angola", "Malanje": "Angola", "Namibe": "Angola", + "Soyo": "Angola", "Cabinda": "Angola", "Uíge": "Angola", "Saurimo": "Angola", + + // Zambia + "Lusaka": "Zambia", "Kitwe": "Zambia", "Ndola": "Zambia", "Kabwe": "Zambia", + "Chingola": "Zambia", "Mufulira": "Zambia", "Livingstone": "Zambia", "Luanshya": "Zambia", + "Kasama": "Zambia", "Chipata": "Zambia", "Mazabuka": "Zambia", "Mongu": "Zambia", + + // Zimbabwe + "Harare": "Zimbabwe", "Bulawayo": "Zimbabwe", "Chitungwiza": "Zimbabwe", "Mutare": "Zimbabwe", + // Namibia + "Windhoek": "Namibia", "Rundu": "Namibia", "Walvis Bay": "Namibia", "Oshakati": "Namibia", + "Swakopmund": "Namibia", "Katima Mulilo": "Namibia", "Grootfontein": "Namibia", "Rehoboth": "Namibia", + "Otjiwarongo": "Namibia", "Okahandja": "Namibia", "Ondangwa": "Namibia", "Outapi": "Namibia", + "Conakry": "Guinea", "Nzérékoré": "Guinea", "Kankan": "Guinea", "Kindia": "Guinea", + // Botswana + "Gaborone": "Botswana", "Francistown": "Botswana", "Molepolole": "Botswana", "Maun": "Botswana", + "Serowe": "Botswana", "Selibe Phikwe": "Botswana", "Kanye": "Botswana", "Mochudi": "Botswana", + "Mahalapye": "Botswana", "Palapye": "Botswana", "Lobatse": "Botswana", "Kasane": "Botswana", + // Guinea-Bissau (补充缺失) + // Lesotho + "Maseru": "Lesotho", "Teyateyaneng": "Lesotho", "Mafeteng": "Lesotho", "Hlotse": "Lesotho", + "Mohale's Hoek": "Lesotho", "Maputsoe": "Lesotho", "Qacha's Nek": "Lesotho", "Quthing": "Lesotho", + "Freetown": "Sierra Leone", "Bo": "Sierra Leone", "Kenema": "Sierra Leone", "Koidu": "Sierra Leone", + // Eswatini (Swaziland) + "Mbabane": "Eswatini", "Manzini": "Eswatini", "Big Bend": "Eswatini", "Malkerns": "Eswatini", + "Nhlangano": "Eswatini", "Siteki": "Eswatini", "Pigg's Peak": "Eswatini", "Lobamba": "Eswatini", + "Monrovia": "Liberia", "Gbarnga": "Liberia", "Kakata": "Liberia", "Bensonville": "Liberia", + // Malawi + "Lilongwe": "Malawi", "Blantyre": "Malawi", "Mzuzu": "Malawi", "Zomba": "Malawi", + "Kasungu": "Malawi", "Mangochi": "Malawi", "Karonga": "Malawi", "Salima": "Malawi", + "Liwonde": "Malawi", "Nkhotakota": "Malawi", "Chiradzulu": "Malawi", "Nsanje": "Malawi", + "Abidjan": "Cote D'Ivoire", "Bouaké": "Cote D'Ivoire", "Daloa": "Cote D'Ivoire", "Yamoussoukro": "Cote D'Ivoire", + // Mozambique + "Maputo": "Mozambique", "Matola": "Mozambique", "Beira": "Mozambique", "Nampula": "Mozambique", + "Chimoio": "Mozambique", "Nacala": "Mozambique", "Quelimane": "Mozambique", "Tete": "Mozambique", + "Xai-Xai": "Mozambique", "Maxixe": "Mozambique", "Inhambane": "Mozambique", "Pemba": "Mozambique", + "Lomé": "Togo", "Sokodé": "Togo", "Kara": "Togo", "Palimé": "Togo", + // Tanzania + "Dar es Salaam": "Tanzania", "Mwanza": "Tanzania", "Arusha": "Tanzania", "Dodoma": "Tanzania", + "Mbeya": "Tanzania", "Morogoro": "Tanzania", "Tanga": "Tanzania", "Kahama": "Tanzania", + "Tabora": "Tanzania", "Zanzibar City": "Tanzania", "Kigoma": "Tanzania", "Sumbawanga": "Tanzania", + "Cotonou": "Benin", "Porto-Novo": "Benin", "Parakou": "Benin", "Djougou": "Benin", + // Rwanda + "Kigali": "Rwanda", "Butare": "Rwanda", "Gitarama": "Rwanda", "Ruhengeri": "Rwanda", + "Gisenyi": "Rwanda", "Byumba": "Rwanda", "Cyangugu": "Rwanda", "Kibuye": "Rwanda", + "Banjul": "Gambia", "Serekunda": "Gambia", "Brikama": "Gambia", "Bakau": "Gambia", + // Burundi + "Gitega": "Burundi", "Bujumbura": "Burundi", "Muyinga": "Burundi", "Ruyigi": "Burundi", + "Ngozi": "Burundi", "Rutana": "Burundi", "Kayanza": "Burundi", "Makamba": "Burundi", + "Nouakchott": "Mauritania", "Nouadhibou": "Mauritania", "Néma": "Mauritania", "Kaédi": "Mauritania", + // Uganda + "Kampala": "Uganda", "Gulu": "Uganda", "Lira": "Uganda", "Mbarara": "Uganda", + "Jinja": "Uganda", "Bwizibwera": "Uganda", "Mbale": "Uganda", "Mukono": "Uganda", + "Kasese": "Uganda", "Masaka": "Uganda", "Entebbe": "Uganda", "Njeru": "Uganda", + // Cabo Verde (补充缺失) + // Tunisia + "Tunis": "Tunisia", "Sfax": "Tunisia", "Sousse": "Tunisia", "Ettadhamen": "Tunisia", + "Kairouan": "Tunisia", "Bizerte": "Tunisia", "Gabès": "Tunisia", "Aryanah": "Tunisia", + "Gafsa": "Tunisia", "El Mourouj": "Tunisia", "Kasserine": "Tunisia", "Ben Arous": "Tunisia", + // São Tomé and Príncipe (补充缺失) + // Libya + "Benghazi": "Libya", "Misrata": "Libya", "Tarhuna": "Libya", + "Al Bayda": "Libya", "Zawiya": "Libya", "Zuwara": "Libya", "Ajdabiya": "Libya", + "Tobruk": "Libya", "Sabha": "Libya", "Sirte": "Libya", "Marj": "Libya", + "Dublin": "Ireland", "Cork": "Ireland", "Limerick": "Ireland", "Galway": "Ireland", + "Waterford": "Ireland", "Drogheda": "Ireland", "Dundalk": "Ireland", "Swords": "Ireland", + "Bray": "Ireland", "Navan": "Ireland", "Ennis": "Ireland", "Kilkenny": "Ireland", + + // More countries and cities can be added here... +} + +// GetCountryCenterByCountryOrCity 根据国家名称或城市名称获取国家中心经纬度 +// countryOrAbbr: 国家名称(全称或简称) +// city: 城市名称 +// 返回: 纬度, 经度, 是否找到 +func GetCountryCenterByCountryOrCity(countryOrAbbr, city string) (lat, lon string, found bool) { + // 1. 首先尝试国家简称转全称(大小写不敏感) + countryOrAbbr = strings.TrimSpace(countryOrAbbr) + if countryOrAbbr != "" { + // 尝试作为简称查找 + if fullName, ok := countryAbbr[strings.ToUpper(countryOrAbbr)]; ok { + countryOrAbbr = fullName + } + // 2. 直接查找国家中心点(大小写不敏感) + for country, center := range countryCenter { + if strings.EqualFold(country, countryOrAbbr) { + return fmt.Sprintf("%f", center[0]), fmt.Sprintf("%f", center[1]), true + } + } + } + + // 3. 通过城市查找国家(大小写不敏感) + city = strings.TrimSpace(city) + if city != "" { + for cityName, country := range cityToCountry { + if strings.EqualFold(cityName, city) { + if center, ok := countryCenter[country]; ok { + return fmt.Sprintf("%f", center[0]), fmt.Sprintf("%f", center[1]), true + } + } + } + } + + return "", "", false +} + +// GetCountryCenter 根据国家名称获取中心经纬度(兼容旧接口) +func GetCountryCenter(countryName string) (lat, lon string, found bool) { + return GetCountryCenterByCountryOrCity(countryName, "") +} + +// GetCountryCenterByCity 根据城市名称获取所在国家的中心经纬度 +func GetCountryCenterByCity(cityName string) (lat, lon string, found bool) { + return GetCountryCenterByCountryOrCity("", cityName) +} diff --git a/pkg/countryCenter/county_center_test.go b/pkg/countryCenter/county_center_test.go new file mode 100644 index 0000000..ef1f8b1 --- /dev/null +++ b/pkg/countryCenter/county_center_test.go @@ -0,0 +1,14 @@ +package countryCenter + +import ( + "testing" +) + +func TestGetCountryCenter(t *testing.T) { + lat, lon, found := GetCountryCenterByCountryOrCity("SG", "Singapore") + if !found { + t.Error("GetCountryCenter('HK') should return found = true") + } + t.Logf("lat = %v, lon = %v", lat, lon) + +} diff --git a/pkg/deduction/deduction.go b/pkg/deduction/deduction.go new file mode 100644 index 0000000..467b90b --- /dev/null +++ b/pkg/deduction/deduction.go @@ -0,0 +1,134 @@ +package deduction + +import ( + "log" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/tool" +) + +const ( + UnitTimeNoLimit = "NoLimit" + UnitTimeYear = "Year" + UnitTimeMonth = "Month" + UnitTimeDay = "Day" + UintTimeHour = "Hour" + UintTimeMinute = "Minute" + + ResetCycleNone = 0 + ResetCycle1st = 1 + ResetCycleMonthly = 2 + ResetCycleYear = 3 +) + +type Subscribe struct { + StartTime time.Time + ExpireTime time.Time + Traffic int64 + Download int64 + Upload int64 + UnitTime string + UnitPrice int64 + ResetCycle int64 + DeductionRatio int64 +} + +type Order struct { + Amount int64 + Quantity int64 +} + +func CalculateRemainingAmount(sub Subscribe, order Order) int64 { + if sub.UnitTime == UnitTimeNoLimit && sub.ResetCycle != 0 { + return 0 + } + log.Printf("开始计算订单剩余价值") + // 实际单价 + sub.UnitPrice = order.Amount / order.Quantity + log.Printf("订阅实际单价: %d", sub.UnitPrice) + now := time.Now() + switch sub.UnitTime { + case UnitTimeNoLimit: + log.Printf("订阅不限时长") + usedTraffic := sub.Traffic - sub.Download - sub.Upload + unitPrice := float64(order.Amount) / float64(sub.Traffic) + return int64(float64(usedTraffic) * unitPrice) + + case UnitTimeYear: + log.Printf("订阅时长为年") + remainingYears := tool.YearDiff(now, sub.ExpireTime) + remainingUnitTimeAmount := calculateRemainingUnitTimeAmount(sub) + return int64(remainingYears)*sub.UnitPrice + remainingUnitTimeAmount + + case UnitTimeMonth: + log.Printf("订阅时长为月") + remainingMonths := tool.MonthDiff(now, sub.ExpireTime) + remainingUnitTimeAmount := calculateRemainingUnitTimeAmount(sub) + return int64(remainingMonths)*sub.UnitPrice + remainingUnitTimeAmount + } + + return 0 +} + +func calculateRemainingUnitTimeAmount(sub Subscribe) int64 { + now := time.Now() + log.Printf("开始计算订阅剩余时长价值") + log.Printf("订阅开始时间: %s, 订阅到期时间: %s,订阅流量: %d", sub.StartTime.Format("2006-01-02 15:04:05"), sub.ExpireTime.Format("2006-01-02 15:04:05"), sub.Traffic) + trafficWeight, timeWeight := calculateWeights(sub.DeductionRatio) + remainingDays, totalDays := getRemainingAndTotalDays(sub, now) + remainingTraffic := sub.Traffic - sub.Download - sub.Upload + remainingTimeAmount := calculateProportionalAmount(sub.UnitPrice, remainingDays, totalDays) + remainingTrafficAmount := calculateProportionalAmount(sub.UnitPrice, remainingTraffic, sub.Traffic) + log.Printf("订阅剩余天数: %d, 总天数: %d, 剩余流量: %d, 剩余时间价值: %d, 剩余流量价值: %d", remainingDays, totalDays, remainingTraffic, remainingTimeAmount, remainingTrafficAmount) + if sub.Traffic == 0 { + return remainingTimeAmount + } + if sub.DeductionRatio != 0 { + return calculateWeightedAmount(sub.UnitPrice, remainingTraffic, sub.Traffic, remainingDays, totalDays, trafficWeight, timeWeight) + } + + return min(remainingTimeAmount, remainingTrafficAmount) +} + +func calculateWeights(deductionRatio int64) (float64, float64) { + if deductionRatio == 0 { + return 0, 0 + } + trafficWeight := float64(deductionRatio) / 100 + timeWeight := 1 - trafficWeight + return trafficWeight, timeWeight +} + +func getRemainingAndTotalDays(sub Subscribe, now time.Time) (int64, int64) { + log.Printf("开始计算订阅剩余天数") + log.Printf("重置周期: %d", sub.ResetCycle) + switch sub.ResetCycle { + case ResetCycleNone: + + remaining := sub.ExpireTime.Sub(now).Hours() / 24 + total := sub.ExpireTime.Sub(sub.StartTime).Hours() / 24 + return int64(remaining), int64(total) + + case ResetCycle1st: + return tool.DaysToNextMonth(now), tool.GetLastDayOfMonth(now) + + case ResetCycleMonthly: + // -1 to include the current day + return tool.DaysToMonthDay(now, sub.StartTime.Day()) - 1, tool.DaysToMonthDay(now, sub.StartTime.Day()) + case ResetCycleYear: + return tool.DaysToYearDay(now, int(sub.StartTime.Month()), sub.StartTime.Day()), + tool.GetYearDays(now, int(sub.StartTime.Month()), sub.StartTime.Day()) + } + return 0, 0 +} + +func calculateWeightedAmount(unitPrice, remainingTraffic, totalTraffic, remainingDays, totalDays int64, trafficWeight, timeWeight float64) int64 { + remainingTimeRatio := float64(remainingDays) / float64(totalDays) + remainingTrafficRatio := float64(remainingTraffic) / float64(totalTraffic) + weightedRemainingRatio := (timeWeight * remainingTimeRatio) + (trafficWeight * remainingTrafficRatio) + return int64(float64(unitPrice) * weightedRemainingRatio) +} + +func calculateProportionalAmount(unitPrice, remaining, total int64) int64 { + return int64(float64(unitPrice) * (float64(remaining) / float64(total))) +} diff --git a/pkg/device/device.go b/pkg/device/device.go new file mode 100644 index 0000000..9aa2239 --- /dev/null +++ b/pkg/device/device.go @@ -0,0 +1,359 @@ +package device + +import ( + "context" + "fmt" + "net/http" + "sync" + "sync/atomic" + "time" + + "go.uber.org/zap" + + "github.com/gorilla/websocket" +) + +type Operator int + +const ( + MaxDevices Operator = iota + Admin + SubscribeUpdate = "subscribe_update" +) + +// Device represents a device structure +type Device struct { + Session string + DeviceID string + Conn *websocket.Conn + CreatedAt time.Time + LastPingTime time.Time +} + +// WebSocket upgrader +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +// DeviceManager manages devices +type DeviceManager struct { + userDevices sync.Map // userID -> []*Device + totalOnline int32 // total online devices + userMutexes sync.Map // userID level locks + heartbeatTimeout int // heartbeat timeout (seconds) + checkInterval int // heartbeat check interval (seconds) + + // event callbacks + OnDeviceOnline func(userID int64, deviceID, session string) + OnDeviceOffline func(userID int64, deviceID, session string, createAt time.Time) + OnDeviceKicked func(userID int64, deviceID, session string, operator Operator) + OnMessage func(userID int64, deviceID, session string, message string) +} + +// Get user-level mutex +func (dm *DeviceManager) getUserMutex(userID int64) *sync.Mutex { + mu, _ := dm.userMutexes.LoadOrStore(userID, &sync.Mutex{}) + return mu.(*sync.Mutex) +} + +// Listen to WebSocket data +func (dm *DeviceManager) listenToDevice(userID int64, device *Device) { + defer func() { + dm.removeDevice(userID, device.DeviceID) // remove device when disconnected + }() + + for { + _, msg, err := device.Conn.ReadMessage() + if err != nil { + zap.S().Infof("Device %s (User %d) disconnected: %v", device.DeviceID, userID, err) + break + } + + message := string(msg) + if message == "ping" || message == "heartbeat" { + dm.UpdateHeartbeat(userID, device.DeviceID) + continue + } + + // Trigger message callback + if dm.OnMessage != nil { + go dm.OnMessage(userID, device.DeviceID, device.Session, message) + } + } +} + +// UpdateHeartbeat updates device heartbeat +func (dm *DeviceManager) UpdateHeartbeat(userID int64, deviceID string) { + mu := dm.getUserMutex(userID) + mu.Lock() + defer mu.Unlock() + + if val, ok := dm.userDevices.Load(userID); ok { + devices := val.([]*Device) + for _, d := range devices { + if d.DeviceID == deviceID { + d.LastPingTime = time.Now() + if err := d.Conn.WriteMessage(websocket.TextMessage, []byte("ping")); err != nil { + zap.S().Infof("✅ Heartbeat updated: Device %s (User %d) err: %s", deviceID, userID, err.Error()) + } + break + } + } + } +} + +// AddDevice **Add: Device connects WebSocket and is added to the manager** +func (dm *DeviceManager) AddDevice(w http.ResponseWriter, r *http.Request, session string, userID int64, deviceID string, maxDevices int) { + // **Upgrade WebSocket connection** + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + zap.S().Infof("WebSocket upgrade failed: %v", err) + return + } + + mu := dm.getUserMutex(userID) + mu.Lock() + defer mu.Unlock() + + newDevice := &Device{ + Session: session, + DeviceID: deviceID, + Conn: conn, + CreatedAt: time.Now(), + LastPingTime: time.Now(), + } + + //不限制设备数量 + if maxDevices < 1 { + maxDevices = 99 + } + + // Get user's device list + var restConnection bool + var devices []*Device + if val, ok := dm.userDevices.Load(userID); ok { + devices = val.([]*Device) + var tempDevice []*Device + for _, d := range devices { + if d.DeviceID == deviceID { + restConnection = true + } else { + tempDevice = append(tempDevice, d) + } + } + devices = tempDevice + } + + // **If exceeding the limit, kick out the earliest device** + if !restConnection && len(devices) >= maxDevices { + oldestDevice := devices[0] + devices = devices[1:] + + if dm.OnDeviceKicked != nil { + done := make(chan struct{}) + go func() { + defer close(done) + dm.OnDeviceKicked(userID, oldestDevice.DeviceID, oldestDevice.Session, MaxDevices) + }() + <-done // block and wait for callback to complete + } + oldestDevice.Conn.Close() + atomic.AddInt32(&dm.totalOnline, -1) + } + + // Add new device + devices = append(devices, newDevice) + dm.userDevices.Store(userID, devices) + atomic.AddInt32(&dm.totalOnline, 1) + + // Trigger online event + if dm.OnDeviceOnline != nil { + go dm.OnDeviceOnline(userID, deviceID, session) + } + + // Start listening + go dm.listenToDevice(userID, newDevice) +} + +// removeDevice removes a device +func (dm *DeviceManager) removeDevice(userID int64, deviceID string) { + mu := dm.getUserMutex(userID) + mu.Lock() + defer mu.Unlock() + + if val, ok := dm.userDevices.Load(userID); ok { + devices := val.([]*Device) + for i, d := range devices { + if d.DeviceID == deviceID { + devices = append(devices[:i], devices[i+1:]...) + d.Conn.Close() + atomic.AddInt32(&dm.totalOnline, -1) + + if dm.OnDeviceOffline != nil { + go dm.OnDeviceOffline(userID, deviceID, d.Session, d.CreatedAt) + } + break + } + } + + if len(devices) == 0 { + dm.userDevices.Delete(userID) + } else { + dm.userDevices.Store(userID, devices) + } + } +} + +// KickDevice kicks a device (supports individual device or entire user) +func (dm *DeviceManager) KickDevice(userID int64, deviceID string) { + mu := dm.getUserMutex(userID) + mu.Lock() + defer mu.Unlock() + + // Get user's device list + val, ok := dm.userDevices.Load(userID) + if !ok { + zap.S().Infof("⚠️ User %d has no online devices, unable to kick out", userID) + return + } + + devices := val.([]*Device) + var activeDevices []*Device + + for _, d := range devices { + if deviceID == "" || d.DeviceID == deviceID { + // Trigger kick event callback + if dm.OnDeviceKicked != nil { + done := make(chan struct{}) + go func() { + defer close(done) + dm.OnDeviceKicked(userID, d.DeviceID, d.Session, Admin) + }() + <-done // block and wait for callback to complete + } + // Close WebSocket connection + d.Conn.Close() + atomic.AddInt32(&dm.totalOnline, -1) + zap.S().Infof("❌ Device %s (User %d) kicked out", d.DeviceID, userID) + } else { + activeDevices = append(activeDevices, d) + } + } + + // Update user's device mapping + if len(activeDevices) == 0 { + dm.userDevices.Delete(userID) + } else { + dm.userDevices.Store(userID, activeDevices) + } +} + +// StartHeartbeatCheck periodically checks for heartbeat timeout devices +func (dm *DeviceManager) StartHeartbeatCheck() { + ticker := time.NewTicker(time.Duration(dm.checkInterval) * time.Second) + defer ticker.Stop() + + for range ticker.C { + now := time.Now() + + dm.userDevices.Range(func(userID, val interface{}) bool { + uid := userID.(int64) + devices := val.([]*Device) + + mu := dm.getUserMutex(uid) + mu.Lock() + defer mu.Unlock() + + var activeDevices []*Device + for _, d := range devices { + if now.Sub(d.LastPingTime) > time.Duration(dm.heartbeatTimeout)*time.Second { + zap.S().Infof("⚠️ Device %s (User %d) heartbeat timeout, removed", d.DeviceID, uid) + d.Conn.Close() + atomic.AddInt32(&dm.totalOnline, -1) + + if dm.OnDeviceOffline != nil { + go dm.OnDeviceOffline(uid, d.DeviceID, d.Session, d.CreatedAt) + } + } else { + activeDevices = append(activeDevices, d) + } + } + + if len(activeDevices) == 0 { + dm.userDevices.Delete(uid) + } else { + dm.userDevices.Store(uid, activeDevices) + } + return true + }) + //zap.S().Infof("Total online devices: %d\n", dm.totalOnline) + } +} + +// NewDeviceManager creates a new device manager +func NewDeviceManager(heartbeatTimeout, checkInterval int) *DeviceManager { + dm := &DeviceManager{ + heartbeatTimeout: heartbeatTimeout, + checkInterval: checkInterval, + } + go dm.StartHeartbeatCheck() + return dm +} + +// SendToDevice sends a message to a specific device +func (dm *DeviceManager) SendToDevice(userID int64, deviceID string, message string) error { + if val, ok := dm.userDevices.Load(userID); ok { + devices := val.([]*Device) + if deviceID == "" { + for _, d := range devices { + err := d.Conn.WriteMessage(websocket.TextMessage, []byte(message)) + if err != nil { + return err + } + continue + } + } else { + for _, d := range devices { + if d.DeviceID == deviceID { + return d.Conn.WriteMessage(websocket.TextMessage, []byte(message)) + } + } + } + + } + return fmt.Errorf("device %s (User %d) is offline", deviceID, userID) +} + +// Broadcast sends a message to all devices +func (dm *DeviceManager) Broadcast(message string) { + go func(message string) { + dm.userDevices.Range(func(_, val interface{}) bool { + devices := val.([]*Device) + for _, d := range devices { + _ = d.Conn.WriteMessage(websocket.TextMessage, []byte(message)) + } + return true + }) + }(message) + +} + +// Gracefully shut down all WebSocket connections +func (dm *DeviceManager) Shutdown(ctx context.Context) { + <-ctx.Done() + zap.S().Infof("🔴 Shutting down all WebSocket connections...") + + dm.userDevices.Range(func(userID, val interface{}) bool { + uid := userID.(int64) + devices := val.([]*Device) + + for _, d := range devices { + d.Conn.Close() + zap.S().Infof("✅ Closed device %s (User %d)", d.DeviceID, uid) + } + dm.userDevices.Delete(uid) + return true + }) +} diff --git a/pkg/device/device_test.go b/pkg/device/device_test.go new file mode 100644 index 0000000..14a6910 --- /dev/null +++ b/pkg/device/device_test.go @@ -0,0 +1,123 @@ +package device + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net" + "net/http" + "strings" + "sync" + "testing" + "time" + + "github.com/pkg/errors" + + "github.com/gorilla/websocket" +) + +func TestDevice(t *testing.T) { + t.Skip("skip test") + /* deviceManager := NewDeviceManager(10, 3) + + deviceManager.OnDeviceOnline = func(userID int64, deviceID, session string) { + fmt.Printf("✅ 设备 %s (用户 %d) 上线\n", deviceID, userID) + } + + deviceManager.OnDeviceOffline = func(userID int64, deviceID, session string) { + fmt.Printf("❌ 设备 %s (用户 %d) 下线\n", deviceID, userID) + } + + deviceManager.OnDeviceKicked = func(userID int64, deviceID, session string, operator Operator) { + fmt.Printf("⚠️ 设备 %s (用户 %d) 被踢下线\n", deviceID, userID) + } + deviceManager.OnMessage = func(userID int64, deviceID, session string, message string) { + log.Printf("✅收到消息: 设备 %s (用户 %d) 内容: %s,sesion: %s\n", deviceID, userID, message, session) + } + engine := gin.Default() + engine.GET("/ws/:userid/:device_number", func(c *gin.Context) { + //根据Authorization获取session + authorization := c.GetHeader("Authorization") + userid, err := strconv.ParseInt(c.Param("userid"), 10, 64) + if err != nil { + t.Errorf("get user id err:%v", err) + return + } + deviceNumber := c.Param("device_number") + deviceManager.AddDevice(c, authorization, userid, deviceNumber, 3) + return + }) + go func() { + err := http.ListenAndServe(":8081", engine) + if err != nil { + t.Fatalf("engine start failed: %v", err) + } + }() + */ + h := http.Header{} + h.Add("Authorization", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJTZXNzaW9uSWQiOiIwMTk0Y2ZiNy1hYjY0LTdjYjMtODUzYi03ZGU5YTAzNWRlZTgiLCJVc2VySWQiOjI5LCJleHAiOjE3MzkyNTY1MDgsImlhdCI6MTczODY1MTcwOH0.BGKT5-hongJPZrA_yAb6cf6go5iDR8T9uu1ZxUg8HDw") + + mutex := sync.Mutex{} + serverURL := fmt.Sprintf("ws://localhost:8080/v1/app/ws/%d/%s", 29, "15502502051") // 假设 userID 为 1001,设备ID 为 deviceA + + // 建立 WebSocket 连接 + conn, resp, err := websocket.DefaultDialer.Dial(serverURL, h) + if err != nil { + all, err := io.ReadAll(resp.Body) + t.Fatalf("websocket dial failed: %v:%s", err, string(all)) + } + // 启动一个 goroutine 来读取服务器消息 + go func() { + for { + _, msg, err := conn.ReadMessage() + if err != nil { + if errors.Is(err, net.ErrClosed) || strings.Contains(err.Error(), "use of closed network connection") { + log.Println("连接已关闭") + return + } + log.Printf("接收消息失败: %v", err) + return + } + fmt.Printf("收到来自服务器的消息: %s\n", msg) + } + }() + + //发送心跳 + go func() { + ticker := time.NewTicker(time.Second * 5) + defer ticker.Stop() + + for range ticker.C { + mutex.Lock() + err := conn.WriteMessage(websocket.TextMessage, []byte("ping")) + mutex.Unlock() + + if err != nil { + if strings.Contains(err.Error(), "use of closed network connection") { + log.Println("连接已关闭") + return + } + t.Errorf("websocket 写入失败: %v", err) + return + } + } + }() + + updateSubscribe, _ := json.Marshal(map[string]interface{}{ + "method": "test_method", + }) + + //发送一条消息 + mutex.Lock() + err = conn.WriteMessage(websocket.TextMessage, updateSubscribe) + mutex.Unlock() + if err != nil { + t.Errorf("websocket write failed: %v", err) + } + + time.Sleep(time.Second * 20) + conn.Close() + time.Sleep(time.Second * 5) + +} diff --git a/pkg/email/platform.go b/pkg/email/platform.go new file mode 100644 index 0000000..95d737c --- /dev/null +++ b/pkg/email/platform.go @@ -0,0 +1,48 @@ +package email + +import "github.com/perfect-panel/ppanel-server/internal/types" + +type Platform int + +const ( + SMTP Platform = iota + unsupported +) + +var platformNames = map[string]Platform{ + "smtp": SMTP, + "unsupported": unsupported, +} + +func (p Platform) String() string { + for k, v := range platformNames { + if v == p { + return k + } + } + return "unsupported" +} + +func parsePlatform(s string) Platform { + if p, ok := platformNames[s]; ok { + return p + } + return unsupported +} + +func GetSupportedPlatforms() []types.PlatformInfo { + return []types.PlatformInfo{ + { + Platform: SMTP.String(), + PlatformUrl: "", + PlatformFieldDescription: map[string]string{ + "host": "host", + "port": "port", + "user": "user", + "pass": "pass", + "from": "from", + "ssl": "ssl", + }, + }, + } +} diff --git a/pkg/email/sender.go b/pkg/email/sender.go new file mode 100644 index 0000000..d1e6570 --- /dev/null +++ b/pkg/email/sender.go @@ -0,0 +1,28 @@ +package email + +import ( + "encoding/json" + "fmt" + + "github.com/perfect-panel/ppanel-server/pkg/email/smtp" + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type Sender interface { + Send(to []string, subject, body string) error +} + +func NewSender(platform, config, siteName string) (Sender, error) { + switch parsePlatform(platform) { + case SMTP: + cfg := smtp.Config{} + if err := json.Unmarshal([]byte(config), &cfg); err != nil { + logger.Error("unmarshal email config failed", logger.Field("error", err.Error()), logger.Field("config", config)) + return nil, err + } + cfg.SiteName = siteName + return smtp.NewClient(&cfg), nil + default: + return nil, fmt.Errorf("unsupported platform: %s", platform) + } +} diff --git a/pkg/email/smtp/email.go b/pkg/email/smtp/email.go new file mode 100644 index 0000000..4a3b498 --- /dev/null +++ b/pkg/email/smtp/email.go @@ -0,0 +1,44 @@ +package smtp + +import ( + "crypto/tls" + + "gopkg.in/gomail.v2" +) + +type Client struct { + conf Config + dailer *gomail.Dialer +} +type Config struct { + Host string `json:"host"` + Port int `json:"port"` + User string `json:"user"` + Pass string `json:"pass"` + From string `json:"from"` + SSL bool `json:"ssl"` + SiteName string `json:"siteName"` +} + +func NewClient(conf *Config) *Client { + if conf == nil { + return nil + } + dailer := gomail.NewDialer(conf.Host, conf.Port, conf.User, conf.Pass) + dailer.TLSConfig = &tls.Config{ + InsecureSkipVerify: true, + MinVersion: tls.VersionTLS12, + ServerName: conf.Host, + } + + return &Client{conf: *conf, dailer: dailer} +} + +func (m *Client) Send(to []string, subject, body string) error { + msg := gomail.NewMessage() + msg.SetAddressHeader("From", m.conf.From, m.conf.SiteName) + msg.SetHeader("To", to...) + msg.SetHeader("Subject", subject) + msg.SetBody("text/html", body) + return m.dailer.DialAndSend(msg) +} diff --git a/pkg/email/smtp/email_test.go b/pkg/email/smtp/email_test.go new file mode 100644 index 0000000..445d9af --- /dev/null +++ b/pkg/email/smtp/email_test.go @@ -0,0 +1,24 @@ +package smtp + +import "testing" + +func TestEmailSend(t *testing.T) { + t.Skipf("Skip TestEmailSend") + config := &Config{ + Host: "smtp.mail.me.com", + Port: 587, + User: "support@ppanel.dev", + Pass: "password", + From: "support@ppanel.dev", + SSL: true, + SiteName: "", + } + address := []string{"tension@sparkdance.dev"} + subject := "test" + body := "test" + email := NewClient(config) + err := email.Send(address, subject, body) + if err != nil { + t.Errorf("send email error: %v", err) + } +} diff --git a/pkg/email/template.go b/pkg/email/template.go new file mode 100644 index 0000000..f31b9c7 --- /dev/null +++ b/pkg/email/template.go @@ -0,0 +1,364 @@ +package email + +const ( + DefaultEmailVerifyTemplate = ` + + + + + + {{if eq .Type 1}}注册验证码 / Registration Verification Code{{else}}重置密码验证码 / Password + Reset Verification Code{{end}} + + + + +
+
+ +

{{.SiteName}}

+
+
+

Hi, 尊敬的用户 / Dear User

+

+ {{if eq .Type 1}} 感谢您注册!您的验证码是(请于{{.Expire}}分钟内使用): +
+ Thank you for registering! Your verification code is (please use it within + {{.Expire}} minutes): {{else}} + 您正在重置密码。您的验证码是(请于{{.Expire}}分钟内使用): +
+ You are resetting your password. Your verification code is (please use it within + {{.Expire}} minutes): {{end}} +

+
+ {{.Code}} +
+

+ 如果您未请求此验证码,请忽略此邮件。
If you did not request this code, please ignore + this email. +

+
+ +
+ + +` + DefaultMaintenanceEmailTemplate = ` + + + + + + 系统维护通知 / System Maintenance Notice + + + +
+
+ +

{{.SiteName}}

+
+
+

Hi, 尊敬的用户 / Dear User

+

+ 我们计划在{{.MaintenanceDate}}进行系统维护,预计维护时间为{{.MaintenanceTime}}。在此期间,您可能会遇到服务中断或无法访问的情况。 +
+ We will be performing system maintenance on + {{.MaintenanceDate}}, and the expected maintenance period + is {{.MaintenanceTime}}. During this time, you may + experience service interruptions or unavailability. +

+

+ 维护完成后,系统将自动恢复。如果您有任何问题,请随时联系我们的支持团队。 +
+ The system will resume automatically once the maintenance is completed. If you have any + questions, please feel free to contact our support team. +

+
+ +
+ + +` + DefaultExpirationEmailTemplate = ` + + + + + 服务到期通知 / Service Expiration Notice + + + +
+
+ +

{{.SiteName}}

+
+
+

Hi, 尊敬的用户 / Dear User

+

+ 您的服务即将在{{.ExpireDate}}到期,请及时续费以保证服务不间断。 +
+ Your service is set to expire on {{.ExpireDate}}. Please + renew your subscription to avoid service interruptions. +

+

+ 如需帮助,请联系客服团队。感谢您的支持! +
+ If you need assistance, please contact our support team. Thank you for your continued + support! +

+
+ +
+ + +` + + DefaultTrafficExceedEmailTemplate = ` + + + + + 流量用尽通知 / Traffic Exhausted Notice + + + +
+
+ +

{{.SiteName}}

+
+
+

Hi, 尊敬的用户 / Dear User

+

+ 您的流量已经用尽,请及时购买流量以继续使用我们的服务。 +
+ Your traffic has been exhausted. Please purchase additional traffic to continue using our + service. +

+

+ 如需帮助,请联系客服团队。感谢您的支持! +
+ If you need assistance, please contact our support team. Thank you for your continued + support! +

+
+ +
+ +` +) diff --git a/pkg/email/template_test.go b/pkg/email/template_test.go new file mode 100644 index 0000000..9c8fd51 --- /dev/null +++ b/pkg/email/template_test.go @@ -0,0 +1,36 @@ +package email + +import ( + "bytes" + "html/template" + "testing" +) + +type VerifyTemplate struct { + Type uint8 + SiteLogo string + SiteName string + Expire uint8 + Code string +} + +func TestVerifyEmail(t *testing.T) { + t.Skipf("Skip TestVerifyEmail test") + data := VerifyTemplate{ + Type: 1, + SiteLogo: "https://www.google.com", + SiteName: "Google", + Expire: 5, + Code: "123456", + } + tpl, err := template.New("email").Parse(DefaultEmailVerifyTemplate) + if err != nil { + t.Error(err) + } + var result bytes.Buffer + err = tpl.Execute(&result, data) + if err != nil { + t.Error(err) + } + t.Log(result.String()) +} diff --git a/pkg/errorx/atomicerror.go b/pkg/errorx/atomicerror.go new file mode 100644 index 0000000..2a3db80 --- /dev/null +++ b/pkg/errorx/atomicerror.go @@ -0,0 +1,23 @@ +package errorx + +import "sync/atomic" + +// AtomicError defines an atomic error. +type AtomicError struct { + err atomic.Value // error +} + +// Set sets the error. +func (ae *AtomicError) Set(err error) { + if err != nil { + ae.err.Store(err) + } +} + +// Load returns the error. +func (ae *AtomicError) Load() error { + if v := ae.err.Load(); v != nil { + return v.(error) + } + return nil +} diff --git a/pkg/errorx/atomicerror_test.go b/pkg/errorx/atomicerror_test.go new file mode 100644 index 0000000..10c7b44 --- /dev/null +++ b/pkg/errorx/atomicerror_test.go @@ -0,0 +1,82 @@ +package errorx + +import ( + "errors" + "sync" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" +) + +var errDummy = errors.New("hello") + +func TestAtomicError(t *testing.T) { + var err AtomicError + err.Set(errDummy) + assert.Equal(t, errDummy, err.Load()) +} + +func TestAtomicErrorSetNil(t *testing.T) { + var ( + errNil error + err AtomicError + ) + err.Set(errNil) + assert.Equal(t, errNil, err.Load()) +} + +func TestAtomicErrorNil(t *testing.T) { + var err AtomicError + assert.Nil(t, err.Load()) +} + +func BenchmarkAtomicError(b *testing.B) { + var aerr AtomicError + wg := sync.WaitGroup{} + + b.Run("Load", func(b *testing.B) { + var done uint32 + go func() { + for { + if atomic.LoadUint32(&done) != 0 { + break + } + wg.Add(1) + go func() { + aerr.Set(errDummy) + wg.Done() + }() + } + }() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = aerr.Load() + } + b.StopTimer() + atomic.StoreUint32(&done, 1) + wg.Wait() + }) + b.Run("Set", func(b *testing.B) { + var done uint32 + go func() { + for { + if atomic.LoadUint32(&done) != 0 { + break + } + wg.Add(1) + go func() { + _ = aerr.Load() + wg.Done() + }() + } + }() + b.ResetTimer() + for i := 0; i < b.N; i++ { + aerr.Set(errDummy) + } + b.StopTimer() + atomic.StoreUint32(&done, 1) + wg.Wait() + }) +} diff --git a/pkg/errorx/batcherror.go b/pkg/errorx/batcherror.go new file mode 100644 index 0000000..60cba03 --- /dev/null +++ b/pkg/errorx/batcherror.go @@ -0,0 +1,42 @@ +package errorx + +import ( + "errors" + "sync" +) + +// BatchError is an error that can hold multiple errors. +type BatchError struct { + errs []error + lock sync.RWMutex +} + +// Add adds one or more non-nil errors to the BatchError instance. +func (be *BatchError) Add(errs ...error) { + be.lock.Lock() + defer be.lock.Unlock() + + for _, err := range errs { + if err != nil { + be.errs = append(be.errs, err) + } + } +} + +// Err returns an error that represents all accumulated errors. +// It returns nil if there are no errors. +func (be *BatchError) Err() error { + be.lock.RLock() + defer be.lock.RUnlock() + + // If there are no non-nil errors, errors.Join(...) returns nil. + return errors.Join(be.errs...) +} + +// NotNil checks if there is at least one error inside the BatchError. +func (be *BatchError) NotNil() bool { + be.lock.RLock() + defer be.lock.RUnlock() + + return len(be.errs) > 0 +} diff --git a/pkg/errorx/batcherror_test.go b/pkg/errorx/batcherror_test.go new file mode 100644 index 0000000..ca6d03b --- /dev/null +++ b/pkg/errorx/batcherror_test.go @@ -0,0 +1,147 @@ +package errorx + +import ( + "errors" + "fmt" + "sync" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + err1 = "first error" + err2 = "second error" +) + +func TestBatchErrorNil(t *testing.T) { + var batch BatchError + assert.Nil(t, batch.Err()) + assert.False(t, batch.NotNil()) + batch.Add(nil) + assert.Nil(t, batch.Err()) + assert.False(t, batch.NotNil()) +} + +func TestBatchErrorNilFromFunc(t *testing.T) { + err := func() error { + var be BatchError + return be.Err() + }() + assert.True(t, err == nil) +} + +func TestBatchErrorOneError(t *testing.T) { + var batch BatchError + batch.Add(errors.New(err1)) + assert.NotNil(t, batch.Err()) + assert.Equal(t, err1, batch.Err().Error()) + assert.True(t, batch.NotNil()) +} + +func TestBatchErrorWithErrors(t *testing.T) { + var batch BatchError + batch.Add(errors.New(err1)) + batch.Add(errors.New(err2)) + assert.NotNil(t, batch.Err()) + assert.Equal(t, fmt.Sprintf("%s\n%s", err1, err2), batch.Err().Error()) + assert.True(t, batch.NotNil()) +} + +func TestBatchErrorConcurrentAdd(t *testing.T) { + const count = 10000 + var batch BatchError + var wg sync.WaitGroup + + wg.Add(count) + for i := 0; i < count; i++ { + go func() { + defer wg.Done() + batch.Add(errors.New(err1)) + }() + } + wg.Wait() + + assert.NotNil(t, batch.Err()) + assert.Equal(t, count, len(batch.errs)) + assert.True(t, batch.NotNil()) +} + +func TestBatchError_Unwrap(t *testing.T) { + t.Run("nil", func(t *testing.T) { + var be BatchError + assert.Nil(t, be.Err()) + assert.True(t, errors.Is(be.Err(), nil)) + }) + + t.Run("one error", func(t *testing.T) { + var errFoo = errors.New("foo") + var errBar = errors.New("bar") + var be BatchError + be.Add(errFoo) + assert.True(t, errors.Is(be.Err(), errFoo)) + assert.False(t, errors.Is(be.Err(), errBar)) + }) + + t.Run("two errors", func(t *testing.T) { + var errFoo = errors.New("foo") + var errBar = errors.New("bar") + var errBaz = errors.New("baz") + var be BatchError + be.Add(errFoo) + be.Add(errBar) + assert.True(t, errors.Is(be.Err(), errFoo)) + assert.True(t, errors.Is(be.Err(), errBar)) + assert.False(t, errors.Is(be.Err(), errBaz)) + }) +} + +func TestBatchError_Add(t *testing.T) { + var be BatchError + + // Test adding nil errors + be.Add(nil, nil) + assert.False(t, be.NotNil(), "Expected BatchError to be empty after adding nil errors") + + // Test adding non-nil errors + err1 := errors.New("error 1") + err2 := errors.New("error 2") + be.Add(err1, err2) + assert.True(t, be.NotNil(), "Expected BatchError to be non-empty after adding errors") + + // Test adding a mix of nil and non-nil errors + err3 := errors.New("error 3") + be.Add(nil, err3, nil) + assert.True(t, be.NotNil(), "Expected BatchError to be non-empty after adding a mix of nil and non-nil errors") +} + +func TestBatchError_Err(t *testing.T) { + var be BatchError + + // Test Err() on empty BatchError + assert.Nil(t, be.Err(), "Expected nil error for empty BatchError") + + // Test Err() with multiple errors + err1 := errors.New("error 1") + err2 := errors.New("error 2") + be.Add(err1, err2) + + combinedErr := be.Err() + assert.NotNil(t, combinedErr, "Expected nil error for BatchError with multiple errors") + + // Check if the combined error contains both error messages + errString := combinedErr.Error() + assert.Truef(t, errors.Is(combinedErr, err1), "Combined error doesn't contain first error: %s", errString) + assert.Truef(t, errors.Is(combinedErr, err2), "Combined error doesn't contain second error: %s", errString) +} + +func TestBatchError_NotNil(t *testing.T) { + var be BatchError + + // Test NotNil() on empty BatchError + assert.Nil(t, be.Err(), "Expected nil error for empty BatchError") + + // Test NotNil() after adding an error + be.Add(errors.New("test error")) + assert.NotNil(t, be.Err(), "Expected non-nil error after adding an error") +} diff --git a/pkg/errorx/callchain.go b/pkg/errorx/callchain.go new file mode 100644 index 0000000..dbdab05 --- /dev/null +++ b/pkg/errorx/callchain.go @@ -0,0 +1,12 @@ +package errorx + +// Chain runs funs one by one until an error occurred. +func Chain(fns ...func() error) error { + for _, fn := range fns { + if err := fn(); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/errorx/callchain_test.go b/pkg/errorx/callchain_test.go new file mode 100644 index 0000000..234dd5c --- /dev/null +++ b/pkg/errorx/callchain_test.go @@ -0,0 +1,27 @@ +package errorx + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestChain(t *testing.T) { + errDummy := errors.New("dummy") + assert.Nil(t, Chain(func() error { + return nil + }, func() error { + return nil + })) + assert.Equal(t, errDummy, Chain(func() error { + return errDummy + }, func() error { + return nil + })) + assert.Equal(t, errDummy, Chain(func() error { + return nil + }, func() error { + return errDummy + })) +} diff --git a/pkg/errorx/check.go b/pkg/errorx/check.go new file mode 100644 index 0000000..b6452b9 --- /dev/null +++ b/pkg/errorx/check.go @@ -0,0 +1,14 @@ +package errorx + +import "errors" + +// In checks if the given err is one of errs. +func In(err error, errs ...error) bool { + for _, each := range errs { + if errors.Is(err, each) { + return true + } + } + + return false +} diff --git a/pkg/errorx/check_test.go b/pkg/errorx/check_test.go new file mode 100644 index 0000000..0e7b267 --- /dev/null +++ b/pkg/errorx/check_test.go @@ -0,0 +1,70 @@ +package errorx + +import ( + "errors" + "testing" +) + +func TestIn(t *testing.T) { + err1 := errors.New("error 1") + err2 := errors.New("error 2") + err3 := errors.New("error 3") + + tests := []struct { + name string + err error + errs []error + want bool + }{ + { + name: "Error matches one of the errors in the list", + err: err1, + errs: []error{err1, err2}, + want: true, + }, + { + name: "Error does not match any errors in the list", + err: err3, + errs: []error{err1, err2}, + want: false, + }, + { + name: "Empty error list", + err: err1, + errs: []error{}, + want: false, + }, + { + name: "Nil error with non-nil list", + err: nil, + errs: []error{err1, err2}, + want: false, + }, + { + name: "Non-nil error with nil in list", + err: err1, + errs: []error{nil, err2}, + want: false, + }, + { + name: "Error matches nil error in the list", + err: nil, + errs: []error{nil, err2}, + want: true, + }, + { + name: "Nil error with empty list", + err: nil, + errs: []error{}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := In(tt.err, tt.errs...); got != tt.want { + t.Errorf("In() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/errorx/wrap.go b/pkg/errorx/wrap.go new file mode 100644 index 0000000..67e8617 --- /dev/null +++ b/pkg/errorx/wrap.go @@ -0,0 +1,21 @@ +package errorx + +import "fmt" + +// Wrap returns an error that wraps err with given message. +func Wrap(err error, message string) error { + if err == nil { + return nil + } + + return fmt.Errorf("%s: %w", message, err) +} + +// Wrapf returns an error that wraps err with given format and args. +func Wrapf(err error, format string, args ...any) error { + if err == nil { + return nil + } + + return fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err) +} diff --git a/pkg/errorx/wrap_test.go b/pkg/errorx/wrap_test.go new file mode 100644 index 0000000..4682c9e --- /dev/null +++ b/pkg/errorx/wrap_test.go @@ -0,0 +1,24 @@ +package errorx + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestWrap(t *testing.T) { + assert.Nil(t, Wrap(nil, "test")) + assert.Equal(t, "foo: bar", Wrap(errors.New("bar"), "foo").Error()) + + err := errors.New("foo") + assert.True(t, errors.Is(Wrap(err, "bar"), err)) +} + +func TestWrapf(t *testing.T) { + assert.Nil(t, Wrapf(nil, "%s", "test")) + assert.Equal(t, "foo bar: quz", Wrapf(errors.New("quz"), "foo %s", "bar").Error()) + + err := errors.New("foo") + assert.True(t, errors.Is(Wrapf(err, "foo %s", "bar"), err)) +} diff --git a/pkg/exchangeRate/exchangeRate.go b/pkg/exchangeRate/exchangeRate.go new file mode 100644 index 0000000..83ce545 --- /dev/null +++ b/pkg/exchangeRate/exchangeRate.go @@ -0,0 +1,54 @@ +package exchangeRate + +import ( + "errors" + "strconv" + "time" + + "github.com/go-resty/resty/v2" +) + +const ( + Url = "https://api.exchangerate.host" +) + +type Response struct { + Success bool `json:"success"` + Terms string `json:"terms"` + Privacy string `json:"privacy"` + Query struct { + From string `json:"from"` + To string `json:"to"` + Amount float64 `json:"amount"` + } `json:"query"` + Info struct { + Timestamp int64 `json:"timestamp"` + Quote float64 `json:"quote"` + } `json:"info"` + Result float64 `json:"result"` +} + +func GetExchangeRete(form, to, access string, amount float64) (float64, error) { + client := resty.New() + client.SetRetryCount(3) + client.SetTimeout(5 * time.Second) + client.SetBaseURL(Url) + // amount to string + amountStr := strconv.FormatFloat(amount, 'f', -1, 64) + + client.SetQueryParams(map[string]string{ + "from": form, + "to": to, + "amount": amountStr, + "access_key": access, + }) + resp := new(Response) + _, err := client.R().SetResult(resp).Get("/convert") + if err != nil { + return 0, err + } + if !resp.Success { + return 0, errors.New("exchange rate failed") + } + return resp.Result, nil +} diff --git a/pkg/exchangeRate/exchange_rate_test.go b/pkg/exchangeRate/exchange_rate_test.go new file mode 100644 index 0000000..c24595d --- /dev/null +++ b/pkg/exchangeRate/exchange_rate_test.go @@ -0,0 +1,12 @@ +package exchangeRate + +import "testing" + +func TestGetExchangeRete(t *testing.T) { + t.Skip("skip TestGetExchangeRete") + result, err := GetExchangeRete("USD", "CNY", "90734e5af4f5353114cdaf3bb9c3f2e3", 1) + if err != nil { + t.Fatal(err) + } + t.Log(result) +} diff --git a/pkg/fs/files+polyfill.go b/pkg/fs/files+polyfill.go new file mode 100644 index 0000000..f726546 --- /dev/null +++ b/pkg/fs/files+polyfill.go @@ -0,0 +1,8 @@ +//go:build windows + +package fs + +import "os" + +func CloseOnExec(*os.File) { +} diff --git a/pkg/fs/files.go b/pkg/fs/files.go new file mode 100644 index 0000000..e58a66a --- /dev/null +++ b/pkg/fs/files.go @@ -0,0 +1,15 @@ +//go:build linux || darwin + +package fs + +import ( + "os" + "syscall" +) + +// CloseOnExec makes sure closing the file on process forking. +func CloseOnExec(file *os.File) { + if file != nil { + syscall.CloseOnExec(int(file.Fd())) + } +} diff --git a/pkg/fs/files_test.go b/pkg/fs/files_test.go new file mode 100644 index 0000000..96ff174 --- /dev/null +++ b/pkg/fs/files_test.go @@ -0,0 +1,15 @@ +package fs + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCloseOnExec(t *testing.T) { + file := os.NewFile(0, os.DevNull) + assert.NotPanics(t, func() { + CloseOnExec(file) + }) +} diff --git a/pkg/fs/temps.go b/pkg/fs/temps.go new file mode 100644 index 0000000..f1ead01 --- /dev/null +++ b/pkg/fs/temps.go @@ -0,0 +1,41 @@ +package fs + +import ( + "os" + + "github.com/perfect-panel/ppanel-server/pkg/hash" +) + +// TempFileWithText creates the temporary file with the given content, +// and returns the opened *os.File instance. +// The file is kept as open, the caller should close the file handle, +// and remove the file by name. +func TempFileWithText(text string) (*os.File, error) { + tmpFile, err := os.CreateTemp(os.TempDir(), hash.Md5Hex([]byte(text))) + if err != nil { + return nil, err + } + + if err := os.WriteFile(tmpFile.Name(), []byte(text), os.ModeTemporary); err != nil { + return nil, err + } + + return tmpFile, nil +} + +// TempFilenameWithText creates the file with the given content, +// and returns the filename (full path). +// The caller should remove the file after use. +func TempFilenameWithText(text string) (string, error) { + tmpFile, err := TempFileWithText(text) + if err != nil { + return "", err + } + + filename := tmpFile.Name() + if err = tmpFile.Close(); err != nil { + return "", err + } + + return filename, nil +} diff --git a/pkg/fs/temps_test.go b/pkg/fs/temps_test.go new file mode 100644 index 0000000..1e2ed6e --- /dev/null +++ b/pkg/fs/temps_test.go @@ -0,0 +1,49 @@ +package fs + +import ( + "io" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTempFileWithText(t *testing.T) { + f, err := TempFileWithText("test") + if err != nil { + t.Error(err) + } + if f == nil { + t.Error("TempFileWithText returned nil") + } + if f.Name() == "" { + t.Error("TempFileWithText returned empty file name") + } + defer os.Remove(f.Name()) + + bs, err := io.ReadAll(f) + assert.Nil(t, err) + if len(bs) != 4 { + t.Error("TempFileWithText returned wrong file size") + } + if f.Close() != nil { + t.Error("TempFileWithText returned error on close") + } +} + +func TestTempFilenameWithText(t *testing.T) { + f, err := TempFilenameWithText("test") + if err != nil { + t.Error(err) + } + if f == "" { + t.Error("TempFilenameWithText returned empty file name") + } + defer os.Remove(f) + + bs, err := os.ReadFile(f) + assert.Nil(t, err) + if len(bs) != 4 { + t.Error("TempFilenameWithText returned wrong file size") + } +} diff --git a/pkg/hash/consistenthash.go b/pkg/hash/consistenthash.go new file mode 100644 index 0000000..0e67126 --- /dev/null +++ b/pkg/hash/consistenthash.go @@ -0,0 +1,186 @@ +package hash + +import ( + "fmt" + "sort" + "strconv" + "sync" + + "github.com/perfect-panel/ppanel-server/pkg/lang" +) + +const ( + // TopWeight is the top weight that one entry might set. + TopWeight = 100 + + minReplicas = 100 + prime = 16777619 +) + +type ( + // Func defines the hash method. + Func func(data []byte) uint64 + + // A ConsistentHash is a ring hash implementation. + ConsistentHash struct { + hashFunc Func + replicas int + keys []uint64 + ring map[uint64][]any + nodes map[string]lang.PlaceholderType + lock sync.RWMutex + } +) + +// NewConsistentHash returns a ConsistentHash. +func NewConsistentHash() *ConsistentHash { + return NewCustomConsistentHash(minReplicas, Hash) +} + +// NewCustomConsistentHash returns a ConsistentHash with given replicas and hash func. +func NewCustomConsistentHash(replicas int, fn Func) *ConsistentHash { + if replicas < minReplicas { + replicas = minReplicas + } + + if fn == nil { + fn = Hash + } + + return &ConsistentHash{ + hashFunc: fn, + replicas: replicas, + ring: make(map[uint64][]any), + nodes: make(map[string]lang.PlaceholderType), + } +} + +// Add adds the node with the number of h.replicas, +// the later call will overwrite the replicas of the former calls. +func (h *ConsistentHash) Add(node any) { + h.AddWithReplicas(node, h.replicas) +} + +// AddWithReplicas adds the node with the number of replicas, +// replicas will be truncated to h.replicas if it's larger than h.replicas, +// the later call will overwrite the replicas of the former calls. +func (h *ConsistentHash) AddWithReplicas(node any, replicas int) { + h.Remove(node) + + if replicas > h.replicas { + replicas = h.replicas + } + + nodeRepr := repr(node) + h.lock.Lock() + defer h.lock.Unlock() + h.addNode(nodeRepr) + + for i := 0; i < replicas; i++ { + hash := h.hashFunc([]byte(nodeRepr + strconv.Itoa(i))) + h.keys = append(h.keys, hash) + h.ring[hash] = append(h.ring[hash], node) + } + + sort.Slice(h.keys, func(i, j int) bool { + return h.keys[i] < h.keys[j] + }) +} + +// AddWithWeight adds the node with weight, the weight can be 1 to 100, indicates the percent, +// the later call will overwrite the replicas of the former calls. +func (h *ConsistentHash) AddWithWeight(node any, weight int) { + // don't need to make sure weight not larger than TopWeight, + // because AddWithReplicas makes sure replicas cannot be larger than h.replicas + replicas := h.replicas * weight / TopWeight + h.AddWithReplicas(node, replicas) +} + +// Get returns the corresponding node from h base on the given v. +func (h *ConsistentHash) Get(v any) (any, bool) { + h.lock.RLock() + defer h.lock.RUnlock() + + if len(h.ring) == 0 { + return nil, false + } + + hash := h.hashFunc([]byte(repr(v))) + index := sort.Search(len(h.keys), func(i int) bool { + return h.keys[i] >= hash + }) % len(h.keys) + + nodes := h.ring[h.keys[index]] + switch len(nodes) { + case 0: + return nil, false + case 1: + return nodes[0], true + default: + innerIndex := h.hashFunc([]byte(innerRepr(v))) + pos := int(innerIndex % uint64(len(nodes))) + return nodes[pos], true + } +} + +// Remove removes the given node from h. +func (h *ConsistentHash) Remove(node any) { + nodeRepr := repr(node) + + h.lock.Lock() + defer h.lock.Unlock() + + if !h.containsNode(nodeRepr) { + return + } + + for i := 0; i < h.replicas; i++ { + hash := h.hashFunc([]byte(nodeRepr + strconv.Itoa(i))) + index := sort.Search(len(h.keys), func(i int) bool { + return h.keys[i] >= hash + }) + if index < len(h.keys) && h.keys[index] == hash { + h.keys = append(h.keys[:index], h.keys[index+1:]...) + } + h.removeRingNode(hash, nodeRepr) + } + + h.removeNode(nodeRepr) +} + +func (h *ConsistentHash) removeRingNode(hash uint64, nodeRepr string) { + if nodes, ok := h.ring[hash]; ok { + newNodes := nodes[:0] + for _, x := range nodes { + if repr(x) != nodeRepr { + newNodes = append(newNodes, x) + } + } + if len(newNodes) > 0 { + h.ring[hash] = newNodes + } else { + delete(h.ring, hash) + } + } +} + +func (h *ConsistentHash) addNode(nodeRepr string) { + h.nodes[nodeRepr] = lang.Placeholder +} + +func (h *ConsistentHash) containsNode(nodeRepr string) bool { + _, ok := h.nodes[nodeRepr] + return ok +} + +func (h *ConsistentHash) removeNode(nodeRepr string) { + delete(h.nodes, nodeRepr) +} + +func innerRepr(node any) string { + return fmt.Sprintf("%d:%v", prime, node) +} + +func repr(node any) string { + return lang.Repr(node) +} diff --git a/pkg/hash/consistenthash_test.go b/pkg/hash/consistenthash_test.go new file mode 100644 index 0000000..f7c5b00 --- /dev/null +++ b/pkg/hash/consistenthash_test.go @@ -0,0 +1,155 @@ +package hash + +import ( + "fmt" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + keySize = 20 + requestSize = 1000 +) + +func BenchmarkConsistentHashGet(b *testing.B) { + ch := NewConsistentHash() + for i := 0; i < keySize; i++ { + ch.Add("localhost:" + strconv.Itoa(i)) + } + + for i := 0; i < b.N; i++ { + ch.Get(i) + } +} + +func TestConsistentHashIncrementalTransfer(t *testing.T) { + prefix := "anything" + create := func() *ConsistentHash { + ch := NewConsistentHash() + for i := 0; i < keySize; i++ { + ch.Add(prefix + strconv.Itoa(i)) + } + return ch + } + + originCh := create() + keys := make(map[int]string, requestSize) + for i := 0; i < requestSize; i++ { + key, ok := originCh.Get(requestSize + i) + assert.True(t, ok) + assert.NotNil(t, key) + keys[i] = key.(string) + } + + node := fmt.Sprintf("%s%d", prefix, keySize) + for i := 0; i < 10; i++ { + laterCh := create() + laterCh.AddWithWeight(node, 10*(i+1)) + + for j := 0; j < requestSize; j++ { + key, ok := laterCh.Get(requestSize + j) + assert.True(t, ok) + assert.NotNil(t, key) + value := key.(string) + assert.True(t, value == keys[j] || value == node) + } + } +} + +func TestConsistentHashTransferOnFailure(t *testing.T) { + index := 41 + keys, newKeys := getKeysBeforeAndAfterFailure(t, "localhost:", index) + var transferred int + for k, v := range newKeys { + if v != keys[k] { + transferred++ + } + } + + ratio := float32(transferred) / float32(requestSize) + assert.True(t, ratio < 2.5/float32(keySize), fmt.Sprintf("%d: %f", index, ratio)) +} + +func TestConsistentHashLeastTransferOnFailure(t *testing.T) { + prefix := "localhost:" + index := 41 + keys, newKeys := getKeysBeforeAndAfterFailure(t, prefix, index) + for k, v := range keys { + newV := newKeys[k] + if v != prefix+strconv.Itoa(index) { + assert.Equal(t, v, newV) + } + } +} + +func TestConsistentHash_Remove(t *testing.T) { + ch := NewConsistentHash() + ch.Add("first") + ch.Add("second") + ch.Remove("first") + for i := 0; i < 100; i++ { + val, ok := ch.Get(i) + assert.True(t, ok) + assert.Equal(t, "second", val) + } +} + +func TestConsistentHash_RemoveInterface(t *testing.T) { + const key = "any" + ch := NewConsistentHash() + node1 := newMockNode(key, 1) + node2 := newMockNode(key, 2) + ch.AddWithWeight(node1, 80) + ch.AddWithWeight(node2, 50) + assert.Equal(t, 1, len(ch.nodes)) + node, ok := ch.Get(1) + assert.True(t, ok) + assert.Equal(t, key, node.(*mockNode).addr) + assert.Equal(t, 2, node.(*mockNode).id) +} + +func getKeysBeforeAndAfterFailure(t *testing.T, prefix string, index int) (map[int]string, map[int]string) { + ch := NewConsistentHash() + for i := 0; i < keySize; i++ { + ch.Add(prefix + strconv.Itoa(i)) + } + + keys := make(map[int]string, requestSize) + for i := 0; i < requestSize; i++ { + key, ok := ch.Get(requestSize + i) + assert.True(t, ok) + assert.NotNil(t, key) + keys[i] = key.(string) + } + + remove := fmt.Sprintf("%s%d", prefix, index) + ch.Remove(remove) + newKeys := make(map[int]string, requestSize) + for i := 0; i < requestSize; i++ { + key, ok := ch.Get(requestSize + i) + assert.True(t, ok) + assert.NotNil(t, key) + assert.NotEqual(t, remove, key) + newKeys[i] = key.(string) + } + + return keys, newKeys +} + +type mockNode struct { + addr string + id int +} + +func newMockNode(addr string, id int) *mockNode { + return &mockNode{ + addr: addr, + id: id, + } +} + +func (n *mockNode) String() string { + return n.addr +} diff --git a/pkg/hash/hash.go b/pkg/hash/hash.go new file mode 100644 index 0000000..8bd87b5 --- /dev/null +++ b/pkg/hash/hash.go @@ -0,0 +1,25 @@ +package hash + +import ( + "crypto/md5" + "fmt" + + "github.com/spaolacci/murmur3" +) + +// Hash returns the hash value of data. +func Hash(data []byte) uint64 { + return murmur3.Sum64(data) +} + +// Md5 returns the md5 bytes of data. +func Md5(data []byte) []byte { + digest := md5.New() + digest.Write(data) + return digest.Sum(nil) +} + +// Md5Hex returns the md5 hex string of data. +func Md5Hex(data []byte) string { + return fmt.Sprintf("%x", Md5(data)) +} diff --git a/pkg/hash/hash_test.go b/pkg/hash/hash_test.go new file mode 100644 index 0000000..5e0962a --- /dev/null +++ b/pkg/hash/hash_test.go @@ -0,0 +1,47 @@ +package hash + +import ( + "crypto/md5" + "fmt" + "hash/fnv" + "math/big" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + text = "hello, world!\n" + md5Digest = "910c8bc73110b0cd1bc5d2bcae782511" +) + +func TestMd5(t *testing.T) { + actual := fmt.Sprintf("%x", Md5([]byte(text))) + assert.Equal(t, md5Digest, actual) +} + +func TestMd5Hex(t *testing.T) { + actual := Md5Hex([]byte(text)) + assert.Equal(t, md5Digest, actual) +} + +func BenchmarkHashFnv(b *testing.B) { + for i := 0; i < b.N; i++ { + h := fnv.New32() + new(big.Int).SetBytes(h.Sum([]byte(text))).Int64() + } +} + +func BenchmarkHashMd5(b *testing.B) { + for i := 0; i < b.N; i++ { + h := md5.New() + bytes := h.Sum([]byte(text)) + new(big.Int).SetBytes(bytes).Int64() + } +} + +func BenchmarkMurmur3(b *testing.B) { + for i := 0; i < b.N; i++ { + Hash([]byte(text)) + } +} diff --git a/pkg/ip/ip.go b/pkg/ip/ip.go new file mode 100644 index 0000000..b39eda4 --- /dev/null +++ b/pkg/ip/ip.go @@ -0,0 +1,179 @@ +package ip + +import ( + "compress/gzip" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "strings" + + "go.uber.org/zap" + + "github.com/andybalholm/brotli" + "github.com/klauspost/compress/zstd" + "github.com/pkg/errors" +) + +// GetIP parses the input domain or IP address and returns the IP address. +func GetIP(input string) ([]string, error) { + // Check if the input is already a valid IP address. + if net.ParseIP(input) != nil { + return []string{input}, nil + } + + // Use net.LookupIP to resolve the domain name. + ips, err := net.LookupIP(input) + if err != nil { + return nil, err + } + + // Convert IP addresses to string format. + var result []string + for _, ip := range ips { + result = append(result, ip.String()) + } + return result, nil +} + +const ( + ipapi = "ipapi.co" + ipbase = "api.ipbase.com" + ipwhois = "ipwhois.app" + ipinfo = "ipinfo.io" +) + +var ( + queryUrls = map[string]bool{ + ipbase: true, + ipapi: true, + ipwhois: true, + ipinfo: true, + } +) + +// GetRegionByIp queries the geolocation of an IP address using supported services. +func GetRegionByIp(ip string) (*GeoLocationResponse, error) { + for service, enabled := range queryUrls { + if enabled { + response, err := fetchGeolocation(service, ip) + if err != nil { + zap.S().Errorf("Failed to fetch geolocation from %s: %v", service, err) + continue + } + return response, nil + } + } + return nil, errors.New("unable to fetch geolocation for the IP") +} + +// fetchGeolocation sends a request to the specified service to retrieve geolocation data. +func fetchGeolocation(service, ip string) (*GeoLocationResponse, error) { + var apiURL string + + // Construct the API URL based on the service. + switch service { + case ipinfo: + apiURL = fmt.Sprintf("https://ipinfo.io/%s/json", ip) + case ipapi: + apiURL = fmt.Sprintf("https://ipapi.co/%s/json", ip) + case ipbase: + apiURL = fmt.Sprintf("https://api.ipbase.com/v1/json/%s", ip) + case ipwhois: + apiURL = fmt.Sprintf("https://ipwhois.app/json/%s", ip) + default: + return nil, fmt.Errorf("unsupported service: %s", service) + } + + // Create the HTTP request. + req, err := http.NewRequest("GET", apiURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %v", err) + } + + setHeaders(req, service) + + // Create the HTTP client and send the request. + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %v", err) + } + defer resp.Body.Close() + + // Decompress the response body based on Content-Encoding. + body, err := decompressResponse(resp) + if err != nil { + return nil, err + } + // Parse the JSON response into GeoLocationResponse. + var location GeoLocationResponse + if err := json.Unmarshal(body, &location); err != nil { + return nil, fmt.Errorf("failed to parse response: %v", err) + } + + // Ensure compatibility between country fields. + if location.Country == "" { + location.Country = location.CountryName + } + + if location.Loc != "" && strings.Contains(location.Loc, ",") { + loc := strings.Split(location.Loc, ",") + location.Latitude = loc[0] + location.Longitude = loc[1] + } + + return &location, nil +} + +// setHeaders sets the necessary headers for the HTTP request. +func setHeaders(req *http.Request, host string) { + req.Header.Set("Host", host) + req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0") + req.Header.Set("Accept", "application/json, text/html, application/xhtml+xml, */*;q=0.8") + req.Header.Set("Accept-Language", "en-US,en;q=0.5") + req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd") + req.Header.Set("Connection", "keep-alive") + req.Header.Set("Upgrade-Insecure-Requests", "1") +} + +// decompressResponse decompresses the HTTP response body based on its Content-Encoding. +func decompressResponse(resp *http.Response) ([]byte, error) { + var reader io.ReadCloser + var err error + + switch resp.Header.Get("Content-Encoding") { + case "gzip": + reader, err = gzip.NewReader(resp.Body) + case "br": + reader = io.NopCloser(brotli.NewReader(resp.Body)) + case "zstd": + decoder, zstdErr := zstd.NewReader(resp.Body) + if zstdErr != nil { + return nil, fmt.Errorf("failed to create zstd decoder: %v", zstdErr) + } + defer decoder.Close() + return io.ReadAll(decoder) + default: + reader = resp.Body + } + + if err != nil { + return nil, fmt.Errorf("failed to create reader: %v", err) + } + defer reader.Close() + + return io.ReadAll(reader) +} + +// GeoLocationResponse represents the geolocation data returned by the API. +type GeoLocationResponse struct { + Country string `json:"country"` + CountryName string `json:"country_name"` + Region string `json:"region"` + City string `json:"city"` + Latitude string `json:"latitude"` + Longitude string `json:"longitude"` + Loc string `json:"loc"` +} diff --git a/pkg/ip/ip_test.go b/pkg/ip/ip_test.go new file mode 100644 index 0000000..f4560d5 --- /dev/null +++ b/pkg/ip/ip_test.go @@ -0,0 +1,34 @@ +package ip + +import ( + "testing" + "time" +) + +func TestGetIPv4(t *testing.T) { + t.Skip("skip TestGetIPv4") + iPv4, err := GetIP("baidu.com") + if err != nil { + t.Fatal(err) + } + + t.Log(iPv4) +} + +func TestGetRegionByIp(t *testing.T) { + t.Skip("skip TestGetRegionByIp") + ips, err := GetIP("122.14.229.128") + if err != nil { + t.Fatal(err) + } + + for _, ip := range ips { + t.Log(ip) + resp, err := GetRegionByIp(ip) + if err != nil { + t.Fatalf("ip: %s,err: %v", ip, err) + } + t.Logf("country: %s,City: %s,latitude:%s, longitude:%s", resp.Country, resp.City, resp.Latitude, resp.Longitude) + } + time.Sleep(3 * time.Second) +} diff --git a/pkg/jsonx/json.go b/pkg/jsonx/json.go new file mode 100644 index 0000000..1b522e5 --- /dev/null +++ b/pkg/jsonx/json.go @@ -0,0 +1,65 @@ +package jsonx + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "strings" +) + +// Marshal marshals v into json bytes. +func Marshal(v any) ([]byte, error) { + return json.Marshal(v) +} + +// MarshalToString marshals v into a string. +func MarshalToString(v any) (string, error) { + data, err := Marshal(v) + if err != nil { + return "", err + } + + return string(data), nil +} + +// Unmarshal unmarshals data bytes into v. +func Unmarshal(data []byte, v any) error { + decoder := json.NewDecoder(bytes.NewReader(data)) + if err := unmarshalUseNumber(decoder, v); err != nil { + return formatError(string(data), err) + } + + return nil +} + +// UnmarshalFromString unmarshals v from str. +func UnmarshalFromString(str string, v any) error { + decoder := json.NewDecoder(strings.NewReader(str)) + if err := unmarshalUseNumber(decoder, v); err != nil { + return formatError(str, err) + } + + return nil +} + +// UnmarshalFromReader unmarshals v from reader. +func UnmarshalFromReader(reader io.Reader, v any) error { + var buf strings.Builder + teeReader := io.TeeReader(reader, &buf) + decoder := json.NewDecoder(teeReader) + if err := unmarshalUseNumber(decoder, v); err != nil { + return formatError(buf.String(), err) + } + + return nil +} + +func unmarshalUseNumber(decoder *json.Decoder, v any) error { + decoder.UseNumber() + return decoder.Decode(v) +} + +func formatError(v string, err error) error { + return fmt.Errorf("string: `%s`, error: `%w`", v, err) +} diff --git a/pkg/jsonx/json_test.go b/pkg/jsonx/json_test.go new file mode 100644 index 0000000..301ff24 --- /dev/null +++ b/pkg/jsonx/json_test.go @@ -0,0 +1,23 @@ +package jsonx + +import "testing" + +type User struct { + Id int64 + Name string + Age int64 +} + +func TestJson(t *testing.T) { + t.Log("TestJson") + user := &User{ + Id: 1, + Name: "test", + Age: 18, + } + b, err := Marshal(user) + if err != nil { + t.Error(err) + } + t.Log(string(b)) +} diff --git a/pkg/jwt/jwt.go b/pkg/jwt/jwt.go new file mode 100644 index 0000000..ae3d027 --- /dev/null +++ b/pkg/jwt/jwt.go @@ -0,0 +1,49 @@ +package jwt + +import ( + "github.com/golang-jwt/jwt/v5" +) + +// Option jwt additional data +type Option struct { + Key string + Val any +} + +// WithOption returns Option with key-value pairs +func WithOption(key string, val any) Option { + return Option{ + Key: key, + Val: val, + } +} + +// NewJwtToken Generate and return jwt token with given data. +func NewJwtToken(secretKey string, iat, seconds int64, opt ...Option) (string, error) { + claims := make(jwt.MapClaims) + claims["exp"] = iat + seconds + claims["iat"] = iat + + for _, v := range opt { + claims[v.Key] = v.Val + } + + token := jwt.New(jwt.SigningMethodHS256) + token.Claims = claims + return token.SignedString([]byte(secretKey)) +} + +// ParseJwtToken Parse jwt token and return claims. +func ParseJwtToken(tokenString, secretKey string) (jwt.MapClaims, error) { + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + return []byte(secretKey), nil + }) + if err != nil { + return nil, err + } + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return nil, jwt.ErrTokenInvalidId + } + return claims, nil +} diff --git a/pkg/jwt/util_test.go b/pkg/jwt/util_test.go new file mode 100644 index 0000000..efefc31 --- /dev/null +++ b/pkg/jwt/util_test.go @@ -0,0 +1,22 @@ +package jwt + +import ( + "testing" + + "github.com/golang-jwt/jwt/v5" + "github.com/pkg/errors" +) + +// TestNewJwtToken test NewJwtToken function +func TestParseJwtToken(t *testing.T) { + token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJEZXZpY2VJZCI6IjM4IiwiZXhwIjoxNzE4MTU2OTQ4LCJpYXQiOjE3MTc1NTIxNDgsInVzZXJJZCI6MX0.4W0nga82kNrfwWjkwcgYAWj4fI4iRc-ZftwVbu-a_kI" + secret := "ae0536f9-6450-4606-8e13-5a19ed505da0" + + claims, err := ParseJwtToken(token, secret) + if err != nil && !errors.Is(err, jwt.ErrTokenExpired) { + t.Errorf("err: %v", err.Error()) + return + } + // parse jwt token success + t.Logf("claims: %v", claims) +} diff --git a/pkg/jwt/verify.go b/pkg/jwt/verify.go new file mode 100644 index 0000000..1405f67 --- /dev/null +++ b/pkg/jwt/verify.go @@ -0,0 +1,18 @@ +package jwt + +import "github.com/golang-jwt/jwt/v5" + +var ( + InvalidToken = jwt.ErrTokenInvalidId + ExpiredToken = jwt.ErrTokenExpired +) + +func VerifyToken(tokenString, secret string) error { + _, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + return []byte(secret), nil + }) + if err != nil { + return err + } + return nil +} diff --git a/pkg/lang/lang.go b/pkg/lang/lang.go new file mode 100644 index 0000000..7a04728 --- /dev/null +++ b/pkg/lang/lang.go @@ -0,0 +1,78 @@ +package lang + +import ( + "fmt" + "reflect" + "strconv" +) + +// Placeholder is a placeholder object that can be used globally. +var Placeholder PlaceholderType + +type ( + // AnyType can be used to hold any type. + AnyType = any + // PlaceholderType represents a placeholder type. + PlaceholderType = struct{} +) + +// Repr returns the string representation of v. +func Repr(v any) string { + if v == nil { + return "" + } + + // if func (v *Type) String() string, we can't use Elem() + switch vt := v.(type) { + case fmt.Stringer: + return vt.String() + } + + val := reflect.ValueOf(v) + for val.Kind() == reflect.Ptr && !val.IsNil() { + val = val.Elem() + } + + return reprOfValue(val) +} + +func reprOfValue(val reflect.Value) string { + switch vt := val.Interface().(type) { + case bool: + return strconv.FormatBool(vt) + case error: + return vt.Error() + case float32: + return strconv.FormatFloat(float64(vt), 'f', -1, 32) + case float64: + return strconv.FormatFloat(vt, 'f', -1, 64) + case fmt.Stringer: + return vt.String() + case int: + return strconv.Itoa(vt) + case int8: + return strconv.Itoa(int(vt)) + case int16: + return strconv.Itoa(int(vt)) + case int32: + return strconv.Itoa(int(vt)) + case int64: + return strconv.FormatInt(vt, 10) + case string: + return vt + case uint: + return strconv.FormatUint(uint64(vt), 10) + case uint8: + return strconv.FormatUint(uint64(vt), 10) + case uint16: + return strconv.FormatUint(uint64(vt), 10) + case uint32: + return strconv.FormatUint(uint64(vt), 10) + case uint64: + return strconv.FormatUint(vt, 10) + case []byte: + return string(vt) + default: + return fmt.Sprint(val.Interface()) + } +} diff --git a/pkg/lang/lang_test.go b/pkg/lang/lang_test.go new file mode 100644 index 0000000..a1ebdc5 --- /dev/null +++ b/pkg/lang/lang_test.go @@ -0,0 +1,156 @@ +package lang + +import ( + "encoding/json" + "errors" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRepr(t *testing.T) { + var ( + f32 float32 = 1.1 + f64 = 2.2 + i8 int8 = 1 + i16 int16 = 2 + i32 int32 = 3 + i64 int64 = 4 + u8 uint8 = 5 + u16 uint16 = 6 + u32 uint32 = 7 + u64 uint64 = 8 + ) + tests := []struct { + v any + expect string + }{ + { + nil, + "", + }, + { + mockStringable{}, + "mocked", + }, + { + new(mockStringable), + "mocked", + }, + { + newMockPtr(), + "mockptr", + }, + { + &mockOpacity{ + val: 1, + }, + "{1}", + }, + { + true, + "true", + }, + { + false, + "false", + }, + { + f32, + "1.1", + }, + { + f64, + "2.2", + }, + { + i8, + "1", + }, + { + i16, + "2", + }, + { + i32, + "3", + }, + { + i64, + "4", + }, + { + u8, + "5", + }, + { + u16, + "6", + }, + { + u32, + "7", + }, + { + u64, + "8", + }, + { + []byte(`abcd`), + "abcd", + }, + { + mockOpacity{val: 1}, + "{1}", + }, + } + + for _, test := range tests { + t.Run(test.expect, func(t *testing.T) { + assert.Equal(t, test.expect, Repr(test.v)) + }) + } +} + +func TestReprOfValue(t *testing.T) { + t.Run("error", func(t *testing.T) { + assert.Equal(t, "error", reprOfValue(reflect.ValueOf(errors.New("error")))) + }) + + t.Run("stringer", func(t *testing.T) { + assert.Equal(t, "1.23", reprOfValue(reflect.ValueOf(json.Number("1.23")))) + }) + + t.Run("int", func(t *testing.T) { + assert.Equal(t, "1", reprOfValue(reflect.ValueOf(1))) + }) + + t.Run("int", func(t *testing.T) { + assert.Equal(t, "1", reprOfValue(reflect.ValueOf("1"))) + }) + + t.Run("int", func(t *testing.T) { + assert.Equal(t, "1", reprOfValue(reflect.ValueOf(uint(1)))) + }) +} + +type mockStringable struct{} + +func (m mockStringable) String() string { + return "mocked" +} + +type mockPtr struct{} + +func newMockPtr() *mockPtr { + return new(mockPtr) +} + +func (m *mockPtr) String() string { + return "mockptr" +} + +type mockOpacity struct { + val int +} diff --git a/pkg/limit/periodlimit.go b/pkg/limit/periodlimit.go new file mode 100644 index 0000000..c212d86 --- /dev/null +++ b/pkg/limit/periodlimit.go @@ -0,0 +1,133 @@ +package limit + +import ( + "context" + _ "embed" + "errors" + "strconv" + "time" + + "github.com/redis/go-redis/v9" +) + +const ( + // Unknown means not initialized state. + Unknown = iota + // Allowed means allowed state. + Allowed + // HitQuota means this request exactly hit the quota. + HitQuota + // OverQuota means passed the quota. + OverQuota + + internalOverQuota = 0 + internalAllowed = 1 + internalHitQuota = 2 +) + +var ( + // ErrUnknownCode is an error that represents unknown status code. + ErrUnknownCode = errors.New("unknown status code") + + //go:embed periodscript.lua + periodLuaScript string + periodScript = redis.NewScript(periodLuaScript) +) + +type ( + // PeriodOption defines the method to customize a PeriodLimit. + PeriodOption func(l *PeriodLimit) + + // A PeriodLimit is used to limit requests during a period of time. + PeriodLimit struct { + period int + quota int + limitStore *redis.Client + keyPrefix string + align bool + } +) + +// NewPeriodLimit returns a PeriodLimit with given parameters. +func NewPeriodLimit(period, quota int, limitStore *redis.Client, keyPrefix string, + opts ...PeriodOption) *PeriodLimit { + limiter := &PeriodLimit{ + period: period, + quota: quota, + limitStore: limitStore, + keyPrefix: keyPrefix, + } + + for _, opt := range opts { + opt(limiter) + } + + return limiter +} + +// Take requests a permit, it returns the permit state. +func (h *PeriodLimit) Take(key string) (int, error) { + return h.TakeCtx(context.Background(), key) +} + +// TakeCtx requests a permit with context, it returns the permit state. +func (h *PeriodLimit) TakeCtx(ctx context.Context, key string) (int, error) { + resp, err := periodScript.Run(ctx, h.limitStore, []string{h.keyPrefix + key}, []string{ + strconv.Itoa(h.quota), + strconv.Itoa(h.calcExpireSeconds()), + }).Result() + + if err != nil { + return Unknown, err + } + + code, ok := resp.(int64) + if !ok { + return Unknown, ErrUnknownCode + } + + switch code { + case internalOverQuota: + return OverQuota, nil + case internalAllowed: + return Allowed, nil + case internalHitQuota: + return HitQuota, nil + default: + return Unknown, ErrUnknownCode + } +} + +func (h *PeriodLimit) calcExpireSeconds() int { + if h.align { + now := time.Now() + _, offset := now.Zone() + unix := now.Unix() + int64(offset) + return h.period - int(unix%int64(h.period)) + } + + return h.period +} + +// Align returns a func to customize a PeriodLimit with alignment. +// For example, if we want to limit end users with 5 sms verification messages every day, +// we need to align with the local timezone and the start of the day. +func Align() PeriodOption { + return func(l *PeriodLimit) { + l.align = true + } +} + +// ParsePermitState returns the permit state by the code. +func (h *PeriodLimit) ParsePermitState(code int) bool { + switch code { + case Allowed: + return true + case HitQuota: + return true + case OverQuota: + return false + default: + return false + } +} diff --git a/pkg/limit/periodlimit_test.go b/pkg/limit/periodlimit_test.go new file mode 100644 index 0000000..1e0f104 --- /dev/null +++ b/pkg/limit/periodlimit_test.go @@ -0,0 +1,71 @@ +package limit + +import ( + "testing" + + "github.com/redis/go-redis/v9" + + "github.com/stretchr/testify/assert" +) + +func TestPeriodLimit_Take(t *testing.T) { + testPeriodLimit(t) +} + +func TestPeriodLimit_TakeWithAlign(t *testing.T) { + testPeriodLimit(t, Align()) +} + +func TestPeriodLimit_RedisUnavailable(t *testing.T) { + //t.Skipf("skip this test because it's not stable") + const ( + seconds = 1 + quota = 5 + ) + rds := redis.NewClient(&redis.Options{ + Addr: "localhost:12345", + }) + + l := NewPeriodLimit(seconds, quota, rds, "periodlimit:") + val, err := l.Take("first") + assert.NotNil(t, err) + assert.Equal(t, 0, val) +} + +func testPeriodLimit(t *testing.T, opts ...PeriodOption) { + store, _ := CreateRedisWithClean(t) + const ( + seconds = 1 + total = 100 + quota = 5 + ) + l := NewPeriodLimit(seconds, quota, store, "periodlimit", opts...) + var allowed, hitQuota, overQuota int + for i := 0; i < total; i++ { + val, err := l.Take("first") + if err != nil { + t.Error(err) + } + switch val { + case Allowed: + allowed++ + case HitQuota: + hitQuota++ + case OverQuota: + overQuota++ + default: + t.Error("unknown status") + } + } + assert.Equal(t, quota-1, allowed) + assert.Equal(t, 1, hitQuota) + assert.Equal(t, total-quota, overQuota) +} + +func TestQuotaFull(t *testing.T) { + rds, _ := CreateRedisWithClean(t) + l := NewPeriodLimit(1, 1, rds, "periodlimit") + val, err := l.Take("first") + assert.Nil(t, err) + assert.Equal(t, HitQuota, val) +} diff --git a/pkg/limit/periodscript.lua b/pkg/limit/periodscript.lua new file mode 100644 index 0000000..7caaabe --- /dev/null +++ b/pkg/limit/periodscript.lua @@ -0,0 +1,14 @@ +-- to be compatible with aliyun redis, we cannot use `local key = KEYS[1]` to reuse the key +local limit = tonumber(ARGV[1]) +local window = tonumber(ARGV[2]) +local current = redis.call("INCRBY", KEYS[1], 1) +if current == 1 then + redis.call("expire", KEYS[1], window) +end +if current < limit then + return 1 +elseif current == limit then + return 2 +else + return 0 +end \ No newline at end of file diff --git a/pkg/limit/tokenlimit.go b/pkg/limit/tokenlimit.go new file mode 100644 index 0000000..ad42241 --- /dev/null +++ b/pkg/limit/tokenlimit.go @@ -0,0 +1,160 @@ +package limit + +import ( + "context" + _ "embed" + "errors" + "fmt" + "strconv" + "sync" + "sync/atomic" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/errorx" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/redis/go-redis/v9" + xrate "golang.org/x/time/rate" +) + +const ( + tokenFormat = "{%s}.tokens" + timestampFormat = "{%s}.ts" + pingInterval = time.Millisecond * 100 +) + +var ( + //go:embed tokenscript.lua + tokenLuaScript string + tokenScript = redis.NewScript(tokenLuaScript) +) + +// A TokenLimiter controls how frequently events are allowed to happen with in one second. +type TokenLimiter struct { + rate int + burst int + store *redis.Client + tokenKey string + timestampKey string + rescueLock sync.Mutex + redisAlive uint32 + monitorStarted bool + rescueLimiter *xrate.Limiter +} + +// NewTokenLimiter returns a new TokenLimiter that allows events up to rate and permits +// bursts of at most burst tokens. +func NewTokenLimiter(rate, burst int, store *redis.Client, key string) *TokenLimiter { + tokenKey := fmt.Sprintf(tokenFormat, key) + timestampKey := fmt.Sprintf(timestampFormat, key) + + return &TokenLimiter{ + rate: rate, + burst: burst, + store: store, + tokenKey: tokenKey, + timestampKey: timestampKey, + redisAlive: 1, + rescueLimiter: xrate.NewLimiter(xrate.Every(time.Second/time.Duration(rate)), burst), + } +} + +// Allow is shorthand for AllowN(time.Now(), 1). +func (lim *TokenLimiter) Allow() bool { + return lim.AllowN(time.Now(), 1) +} + +// AllowCtx is shorthand for AllowNCtx(ctx,time.Now(), 1) with incoming context. +func (lim *TokenLimiter) AllowCtx(ctx context.Context) bool { + return lim.AllowNCtx(ctx, time.Now(), 1) +} + +// AllowN reports whether n events may happen at time now. +// Use this method if you intend to drop / skip events that exceed the rate. +// Otherwise, use Reserve or Wait. +func (lim *TokenLimiter) AllowN(now time.Time, n int) bool { + return lim.reserveN(context.Background(), now, n) +} + +// AllowNCtx reports whether n events may happen at time now with incoming context. +// Use this method if you intend to drop / skip events that exceed the rate. +// Otherwise, use Reserve or Wait. +func (lim *TokenLimiter) AllowNCtx(ctx context.Context, now time.Time, n int) bool { + return lim.reserveN(ctx, now, n) +} + +func (lim *TokenLimiter) reserveN(ctx context.Context, now time.Time, n int) bool { + if atomic.LoadUint32(&lim.redisAlive) == 0 { + return lim.rescueLimiter.AllowN(now, n) + } + + resp, err := tokenScript.Run(ctx, lim.store, []string{lim.tokenKey, lim.timestampKey}, []string{ + strconv.Itoa(lim.rate), + strconv.Itoa(lim.burst), + strconv.FormatInt(now.Unix(), 10), + strconv.Itoa(n), + }).Result() + // redis allowed == false + // Lua boolean false -> r Nil bulk reply + if errors.Is(err, redis.Nil) { + return false + } + if errorx.In(err, context.DeadlineExceeded, context.Canceled) { + logger.WithContext(ctx).Error("fail to use rate limiter", logger.Field("error", err.Error())) + return false + } + if err != nil { + //log.Errorf(ctx, "fail to eval redis script: %v, use in-process limiter for rescue", err) + lim.startMonitor() + return lim.rescueLimiter.AllowN(now, n) + } + + code, ok := resp.(int64) + if !ok { + logger.Error("fail to eval redis script, use in-process limiter for rescue", logger.Field("response", resp)) + lim.startMonitor() + return lim.rescueLimiter.AllowN(now, n) + } + + // redis allowed == true + // Lua boolean true -> r integer reply with value of 1 + return code == 1 +} + +func (lim *TokenLimiter) startMonitor() { + lim.rescueLock.Lock() + defer lim.rescueLock.Unlock() + + if lim.monitorStarted { + return + } + + lim.monitorStarted = true + atomic.StoreUint32(&lim.redisAlive, 0) + + go lim.waitForRedis() +} + +func (lim *TokenLimiter) waitForRedis() { + ticker := time.NewTicker(pingInterval) + defer func() { + ticker.Stop() + lim.rescueLock.Lock() + lim.monitorStarted = false + lim.rescueLock.Unlock() + }() + + for range ticker.C { + if lim.StorePingCtx() { + atomic.StoreUint32(&lim.redisAlive, 1) + return + } + } +} + +func (lim *TokenLimiter) StorePingCtx() bool { + v, err := lim.store.Ping(context.Background()).Result() + if err != nil { + return false + } + return v == "PONG" +} diff --git a/pkg/limit/tokenlimit_test.go b/pkg/limit/tokenlimit_test.go new file mode 100644 index 0000000..5dbfab2 --- /dev/null +++ b/pkg/limit/tokenlimit_test.go @@ -0,0 +1,80 @@ +package limit + +import ( + "context" + "testing" + "time" + + "github.com/redis/go-redis/v9" + + "github.com/alicebob/miniredis/v2" + "github.com/stretchr/testify/assert" +) + +func TestTokenLimit_WithCtx(t *testing.T) { + const ( + total = 100 + rate = 5 + burst = 10 + ) + store, _ := CreateRedisWithClean(t) + l := NewTokenLimiter(rate, burst, store, "tokenlimit") + + ctx, cancel := context.WithCancel(context.Background()) + ok := l.AllowCtx(ctx) + assert.True(t, ok) + + cancel() + for i := 0; i < total; i++ { + ok := l.AllowCtx(ctx) + assert.False(t, ok) + assert.False(t, l.monitorStarted) + } +} + +func TestTokenLimit_Take(t *testing.T) { + store, _ := CreateRedisWithClean(t) + + const ( + total = 100 + rate = 5 + burst = 10 + ) + l := NewTokenLimiter(rate, burst, store, "tokenlimit") + var allowed int + for i := 0; i < total; i++ { + time.Sleep(time.Second / time.Duration(total)) + if l.Allow() { + allowed++ + } + } + + assert.True(t, allowed >= burst+rate) +} + +func TestTokenLimit_TakeBurst(t *testing.T) { + store, _ := CreateRedisWithClean(t) + + const ( + total = 100 + rate = 5 + burst = 10 + ) + l := NewTokenLimiter(rate, burst, store, "tokenlimit") + var allowed int + for i := 0; i < total; i++ { + if l.Allow() { + allowed++ + } + } + + assert.True(t, allowed >= burst) +} + +// CreateRedisWithClean returns an in process redis.Redis and a clean function. +func CreateRedisWithClean(t *testing.T) (r *redis.Client, clean func()) { + mr := miniredis.RunT(t) + return redis.NewClient(&redis.Options{ + Addr: mr.Addr(), + }), mr.Close +} diff --git a/pkg/limit/tokenscript.lua b/pkg/limit/tokenscript.lua new file mode 100644 index 0000000..ac39565 --- /dev/null +++ b/pkg/limit/tokenscript.lua @@ -0,0 +1,31 @@ +-- to be compatible with aliyun redis, we cannot use `local key = KEYS[1]` to reuse the key +-- KEYS[1] as tokens_key +-- KEYS[2] as timestamp_key +local rate = tonumber(ARGV[1]) +local capacity = tonumber(ARGV[2]) +local now = tonumber(ARGV[3]) +local requested = tonumber(ARGV[4]) +local fill_time = capacity/rate +local ttl = math.floor(fill_time*2) +local last_tokens = tonumber(redis.call("get", KEYS[1])) +if last_tokens == nil then + last_tokens = capacity +end + +local last_refreshed = tonumber(redis.call("get", KEYS[2])) +if last_refreshed == nil then + last_refreshed = 0 +end + +local delta = math.max(0, now-last_refreshed) +local filled_tokens = math.min(capacity, last_tokens+(delta*rate)) +local allowed = filled_tokens >= requested +local new_tokens = filled_tokens +if allowed then + new_tokens = filled_tokens - requested +end + +redis.call("setex", KEYS[1], ttl, new_tokens) +redis.call("setex", KEYS[2], ttl, now) + +return allowed \ No newline at end of file diff --git a/pkg/logger/color.go b/pkg/logger/color.go new file mode 100644 index 0000000..fa33495 --- /dev/null +++ b/pkg/logger/color.go @@ -0,0 +1,26 @@ +package logger + +import ( + "sync/atomic" + + "github.com/perfect-panel/ppanel-server/pkg/color" +) + +// WithColor is a helper function to add color to a string, only in plain encoding. +func WithColor(text string, colour color.Color) string { + if atomic.LoadUint32(&encoding) == plainEncodingType { + return color.WithColor(text, colour) + } + + return text +} + +// WithColorPadding is a helper function to add color to a string with leading and trailing spaces, +// only in plain encoding. +func WithColorPadding(text string, colour color.Color) string { + if atomic.LoadUint32(&encoding) == plainEncodingType { + return color.WithColorPadding(text, colour) + } + + return text +} diff --git a/pkg/logger/color_test.go b/pkg/logger/color_test.go new file mode 100644 index 0000000..b6de740 --- /dev/null +++ b/pkg/logger/color_test.go @@ -0,0 +1,33 @@ +package logger + +import ( + "sync/atomic" + "testing" + + "github.com/perfect-panel/ppanel-server/pkg/color" + "github.com/stretchr/testify/assert" +) + +func TestWithColor(t *testing.T) { + old := atomic.SwapUint32(&encoding, plainEncodingType) + defer atomic.StoreUint32(&encoding, old) + + output := WithColor("hello", color.BgBlue) + assert.Equal(t, "hello", output) + + atomic.StoreUint32(&encoding, jsonEncodingType) + output = WithColor("hello", color.BgBlue) + assert.Equal(t, "hello", output) +} + +func TestWithColorPadding(t *testing.T) { + old := atomic.SwapUint32(&encoding, plainEncodingType) + defer atomic.StoreUint32(&encoding, old) + + output := WithColorPadding("hello", color.BgBlue) + assert.Equal(t, " hello ", output) + + atomic.StoreUint32(&encoding, jsonEncodingType) + output = WithColorPadding("hello", color.BgBlue) + assert.Equal(t, "hello", output) +} diff --git a/pkg/logger/config.go b/pkg/logger/config.go new file mode 100644 index 0000000..8f75b02 --- /dev/null +++ b/pkg/logger/config.go @@ -0,0 +1,47 @@ +package logger + +// A LogConf is a logging config. +type LogConf struct { + // ServiceName represents the service name. + ServiceName string `yaml:"ServiceName" default:"PPanel"` + // Mode represents the logging mode, default is `console`. + // console: log to console. + // file: log to file. + // volume: used in k8s, prepend the hostname to the log file name. + Mode string `yaml:"Mode" default:"console"` + // Encoding represents the encoding type, default is `json`. + // json: json encoding. + // plain: plain text encoding, typically used in development. + Encoding string `yaml:"Encoding" default:"json"` + // TimeFormat represents the time format, default is `2006-01-02T15:04:05.000Z07:00`. + TimeFormat string `yaml:"TimeFormat" default:"2006-01-02T15:04:05.000Z07:00"` + // Path represents the log file path, default is `logs`. + Path string `yaml:"Path" default:"logs"` + // Level represents the log level, default is `info`. + Level string `yaml:"Level" default:"info"` + // MaxContentLength represents the max content bytes, default is no limit. + MaxContentLength uint32 `yaml:"MaxContentLength" default:"0"` + // Compress represents whether to compress the log file, default is `false`. + Compress bool `yaml:"Compress" default:"false"` + // Stat represents whether to log statistics, default is `true`. + Stat bool `yaml:"Stat" default:"true"` + // KeepDays represents how many days the log files will be kept. Default to keep all files. + // Only take effect when Mode is `file` or `volume`, both work when Rotation is `daily` or `size`. + KeepDays int `yaml:"KeepDays" default:"0"` + // StackCooldownMillis represents the cooldown time for stack logging, default is 100ms. + StackCooldownMillis int `yaml:"StackCooldownMillis" default:"100"` + // MaxBackups represents how many backup log files will be kept. 0 means all files will be kept forever. + // Only take effect when RotationRuleType is `size`. + // Even though `MaxBackups` sets 0, log files will still be removed + // if the `KeepDays` limitation is reached. + MaxBackups int `yaml:"MaxBackups" default:"0"` + // MaxSize represents how much space the writing log file takes up. 0 means no limit. The unit is `MB`. + // Only take effect when RotationRuleType is `size` + MaxSize int `yaml:"MaxSize" default:"0"` + // Rotation represents the type of log rotation rule. Default is `daily`. + // daily: daily rotation. + // size: size limited rotation. + Rotation string `yaml:"Rotation" default:"daily"` + // FileTimeFormat represents the time format for file name, default is `2006-01-02T15:04:05.000Z07:00`. + FileTimeFormat string `yaml:"FileTimeFormat" default:"2006-01-02T15:04:05.000Z07:00"` +} diff --git a/pkg/logger/fields.go b/pkg/logger/fields.go new file mode 100644 index 0000000..26c28fd --- /dev/null +++ b/pkg/logger/fields.go @@ -0,0 +1,48 @@ +package logger + +import ( + "context" + "sync" + "sync/atomic" +) + +var ( + fieldsContextKey contextKey + globalFields atomic.Value + globalFieldsLock sync.Mutex +) + +type contextKey struct{} + +// AddGlobalFields adds global fields. +func AddGlobalFields(fields ...LogField) { + globalFieldsLock.Lock() + defer globalFieldsLock.Unlock() + + old := globalFields.Load() + if old == nil { + globalFields.Store(append([]LogField(nil), fields...)) + } else { + globalFields.Store(append(old.([]LogField), fields...)) + } +} + +// ContextWithFields returns a new context with the given fields. +func ContextWithFields(ctx context.Context, fields ...LogField) context.Context { + if val := ctx.Value(fieldsContextKey); val != nil { + if arr, ok := val.([]LogField); ok { + allFields := make([]LogField, 0, len(arr)+len(fields)) + allFields = append(allFields, arr...) + allFields = append(allFields, fields...) + return context.WithValue(ctx, fieldsContextKey, allFields) + } + } + + return context.WithValue(ctx, fieldsContextKey, fields) +} + +// WithFields returns a new logger with the given fields. +// deprecated: use ContextWithFields instead. +func WithFields(ctx context.Context, fields ...LogField) context.Context { + return ContextWithFields(ctx, fields...) +} diff --git a/pkg/logger/fields_test.go b/pkg/logger/fields_test.go new file mode 100644 index 0000000..eabfa38 --- /dev/null +++ b/pkg/logger/fields_test.go @@ -0,0 +1,122 @@ +package logger + +import ( + "bytes" + "context" + "encoding/json" + "strconv" + "sync" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAddGlobalFields(t *testing.T) { + var buf bytes.Buffer + writer := NewWriter(&buf) + old := Reset() + SetWriter(writer) + defer SetWriter(old) + + Info("hello") + buf.Reset() + + AddGlobalFields(Field("a", "1"), Field("b", "2")) + AddGlobalFields(Field("c", "3")) + Info("world") + var m map[string]any + assert.NoError(t, json.Unmarshal(buf.Bytes(), &m)) + assert.Equal(t, "1", m["a"]) + assert.Equal(t, "2", m["b"]) + assert.Equal(t, "3", m["c"]) +} + +func TestContextWithFields(t *testing.T) { + ctx := ContextWithFields(context.Background(), Field("a", 1), Field("b", 2)) + vals := ctx.Value(fieldsContextKey) + assert.NotNil(t, vals) + fields, ok := vals.([]LogField) + assert.True(t, ok) + assert.EqualValues(t, []LogField{Field("a", 1), Field("b", 2)}, fields) +} + +func TestWithFields(t *testing.T) { + ctx := WithFields(context.Background(), Field("a", 1), Field("b", 2)) + vals := ctx.Value(fieldsContextKey) + assert.NotNil(t, vals) + fields, ok := vals.([]LogField) + assert.True(t, ok) + assert.EqualValues(t, []LogField{Field("a", 1), Field("b", 2)}, fields) +} + +func TestWithFieldsAppend(t *testing.T) { + type ctxKey string + var dummyKey ctxKey = "dummyKey" + ctx := context.WithValue(context.Background(), dummyKey, "dummy") + ctx = ContextWithFields(ctx, Field("a", 1), Field("b", 2)) + ctx = ContextWithFields(ctx, Field("c", 3), Field("d", 4)) + vals := ctx.Value(fieldsContextKey) + assert.NotNil(t, vals) + fields, ok := vals.([]LogField) + assert.True(t, ok) + assert.Equal(t, "dummy", ctx.Value(dummyKey)) + assert.EqualValues(t, []LogField{ + Field("a", 1), + Field("b", 2), + Field("c", 3), + Field("d", 4), + }, fields) +} + +func TestWithFieldsAppendCopy(t *testing.T) { + const count = 10 + ctx := context.Background() + for i := 0; i < count; i++ { + ctx = ContextWithFields(ctx, Field(strconv.Itoa(i), 1)) + } + + af := Field("foo", 1) + bf := Field("bar", 2) + ctxa := ContextWithFields(ctx, af) + ctxb := ContextWithFields(ctx, bf) + + assert.EqualValues(t, af, ctxa.Value(fieldsContextKey).([]LogField)[count]) + assert.EqualValues(t, bf, ctxb.Value(fieldsContextKey).([]LogField)[count]) +} + +func BenchmarkAtomicValue(b *testing.B) { + b.ReportAllocs() + + var container atomic.Value + vals := []LogField{ + Field("a", "b"), + Field("c", "d"), + Field("e", "f"), + } + container.Store(&vals) + + for i := 0; i < b.N; i++ { + val := container.Load() + if val != nil { + _ = *val.(*[]LogField) + } + } +} + +func BenchmarkRWMutex(b *testing.B) { + b.ReportAllocs() + + var lock sync.RWMutex + vals := []LogField{ + Field("a", "b"), + Field("c", "d"), + Field("e", "f"), + } + + for i := 0; i < b.N; i++ { + lock.RLock() + _ = vals + lock.RUnlock() + } +} diff --git a/pkg/logger/fs.go b/pkg/logger/fs.go new file mode 100644 index 0000000..d7694d5 --- /dev/null +++ b/pkg/logger/fs.go @@ -0,0 +1,40 @@ +package logger + +import ( + "io" + "os" +) + +var fileSys realFileSystem + +type ( + fileSystem interface { + Close(closer io.Closer) error + Copy(writer io.Writer, reader io.Reader) (int64, error) + Create(name string) (*os.File, error) + Open(name string) (*os.File, error) + Remove(name string) error + } + + realFileSystem struct{} +) + +func (fs realFileSystem) Close(closer io.Closer) error { + return closer.Close() +} + +func (fs realFileSystem) Copy(writer io.Writer, reader io.Reader) (int64, error) { + return io.Copy(writer, reader) +} + +func (fs realFileSystem) Create(name string) (*os.File, error) { + return os.Create(name) +} + +func (fs realFileSystem) Open(name string) (*os.File, error) { + return os.Open(name) +} + +func (fs realFileSystem) Remove(name string) error { + return os.Remove(name) +} diff --git a/pkg/logger/gorm.go b/pkg/logger/gorm.go new file mode 100644 index 0000000..7dbdd4a --- /dev/null +++ b/pkg/logger/gorm.go @@ -0,0 +1,67 @@ +package logger + +import ( + "context" + "fmt" + "time" + + "gorm.io/gorm/logger" +) + +type GormLogger struct { +} + +const TAG = "[GORM]" + +func (l *GormLogger) LogMode(logger.LogLevel) logger.Interface { + var sysLevel string + switch logLevel { + case DebugLevel: + sysLevel = "debug" + case InfoLevel: + sysLevel = "info" + case SevereLevel: + sysLevel = "severe" + case disableLevel: + sysLevel = "disable" + default: + sysLevel = "unknown" + } + Infof("%s System Log Level is %s", TAG, sysLevel) + return l +} + +func (l *GormLogger) Info(ctx context.Context, str string, args ...interface{}) { + WithContext(ctx).WithCallerSkip(6).Infof("%s Info: %s", TAG, str, args) +} + +func (l *GormLogger) Warn(ctx context.Context, str string, args ...interface{}) { + WithContext(ctx).WithCallerSkip(6).Infof("%s Warn: %s", TAG, str, args) +} + +func (l *GormLogger) Error(ctx context.Context, str string, args ...interface{}) { + WithContext(ctx).WithCallerSkip(6).Errorf("%s Error: %s", TAG, str, args) +} + +func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) { + sql, rowsAffected := fc() + fields := []LogField{ + { + Key: "sql", + Value: sql, + }, + { + Key: "rows", + Value: rowsAffected, + }, + } + if err != nil { + fields = append(fields, LogField{ + Key: "error", + Value: err.Error(), + }) + WithContext(ctx).WithCallerSkip(6).WithDuration(time.Since(begin)).Errorw(TAG, fields...) + } else { + WithContext(ctx).WithCallerSkip(6).WithDuration(time.Since(begin)).Infow(fmt.Sprintf("%s SQL Executed", TAG), fields...) + } +} diff --git a/pkg/logger/lesslogger.go b/pkg/logger/lesslogger.go new file mode 100644 index 0000000..db35048 --- /dev/null +++ b/pkg/logger/lesslogger.go @@ -0,0 +1,27 @@ +package logger + +// A LessLogger is a logger that controls to log once during the given duration. +type LessLogger struct { + *limitedExecutor +} + +// NewLessLogger returns a LessLogger. +func NewLessLogger(milliseconds int) *LessLogger { + return &LessLogger{ + limitedExecutor: newLimitedExecutor(milliseconds), + } +} + +// Error logs v into error log or discard it if more than once in the given duration. +func (logger *LessLogger) Error(v ...any) { + logger.logOrDiscard(func() { + Error(v...) + }) +} + +// Errorf logs v with format into error log or discard it if more than once in the given duration. +func (logger *LessLogger) Errorf(format string, v ...any) { + logger.logOrDiscard(func() { + Errorf(format, v...) + }) +} diff --git a/pkg/logger/lesslogger_test.go b/pkg/logger/lesslogger_test.go new file mode 100644 index 0000000..1134c0c --- /dev/null +++ b/pkg/logger/lesslogger_test.go @@ -0,0 +1,35 @@ +package logger + +import ( + "log" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLessLogger_Error(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + l := NewLessLogger(500) + for i := 0; i < 100; i++ { + l.Error("hello") + } + log.Print(w.String()) + assert.Equal(t, 1, strings.Count(w.String(), "\n")) +} + +func TestLessLogger_Errorf(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + l := NewLessLogger(500) + for i := 0; i < 100; i++ { + l.Errorf("hello") + } + + assert.Equal(t, 1, strings.Count(w.String(), "\n")) +} diff --git a/pkg/logger/lesswriter.go b/pkg/logger/lesswriter.go new file mode 100644 index 0000000..a5e6c9b --- /dev/null +++ b/pkg/logger/lesswriter.go @@ -0,0 +1,22 @@ +package logger + +import "io" + +type lessWriter struct { + *limitedExecutor + writer io.Writer +} + +func newLessWriter(writer io.Writer, milliseconds int) *lessWriter { + return &lessWriter{ + limitedExecutor: newLimitedExecutor(milliseconds), + writer: writer, + } +} + +func (w *lessWriter) Write(p []byte) (n int, err error) { + w.logOrDiscard(func() { + _, _ = w.writer.Write(p) + }) + return len(p), nil +} diff --git a/pkg/logger/lesswriter_test.go b/pkg/logger/lesswriter_test.go new file mode 100644 index 0000000..c522bab --- /dev/null +++ b/pkg/logger/lesswriter_test.go @@ -0,0 +1,19 @@ +package logger + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLessWriter(t *testing.T) { + var builder strings.Builder + w := newLessWriter(&builder, 500) + for i := 0; i < 100; i++ { + _, err := w.Write([]byte("hello")) + assert.Nil(t, err) + } + + assert.Equal(t, "hello", builder.String()) +} diff --git a/pkg/logger/limitedexecutor.go b/pkg/logger/limitedexecutor.go new file mode 100644 index 0000000..6db3271 --- /dev/null +++ b/pkg/logger/limitedexecutor.go @@ -0,0 +1,42 @@ +package logger + +import ( + "sync/atomic" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/syncx" + "github.com/perfect-panel/ppanel-server/pkg/timex" +) + +type limitedExecutor struct { + threshold time.Duration + lastTime *syncx.AtomicDuration + discarded uint32 +} + +func newLimitedExecutor(milliseconds int) *limitedExecutor { + return &limitedExecutor{ + threshold: time.Duration(milliseconds) * time.Millisecond, + lastTime: syncx.NewAtomicDuration(), + } +} + +func (le *limitedExecutor) logOrDiscard(execute func()) { + if le == nil || le.threshold <= 0 { + execute() + return + } + + now := timex.Now() + if now-le.lastTime.Load() <= le.threshold { + atomic.AddUint32(&le.discarded, 1) + } else { + le.lastTime.Set(now) + discarded := atomic.SwapUint32(&le.discarded, 0) + if discarded > 0 { + Errorf("Discarded %d error messages", discarded) + } + + execute() + } +} diff --git a/pkg/logger/limitedexecutor_test.go b/pkg/logger/limitedexecutor_test.go new file mode 100644 index 0000000..34af915 --- /dev/null +++ b/pkg/logger/limitedexecutor_test.go @@ -0,0 +1,62 @@ +package logger + +import ( + "sync/atomic" + "testing" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/timex" + "github.com/stretchr/testify/assert" +) + +func TestLimitedExecutor_logOrDiscard(t *testing.T) { + tests := []struct { + name string + threshold time.Duration + lastTime time.Duration + discarded uint32 + executed bool + }{ + { + name: "nil executor", + executed: true, + }, + { + name: "regular", + threshold: time.Hour, + lastTime: timex.Now(), + discarded: 10, + executed: false, + }, + { + name: "slow", + threshold: time.Duration(1), + lastTime: -1000, + discarded: 10, + executed: true, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + executor := newLimitedExecutor(0) + executor.threshold = test.threshold + executor.discarded = test.discarded + executor.lastTime.Set(test.lastTime) + + var run int32 + executor.logOrDiscard(func() { + atomic.AddInt32(&run, 1) + }) + if test.executed { + assert.Equal(t, int32(1), atomic.LoadInt32(&run)) + } else { + assert.Equal(t, int32(0), atomic.LoadInt32(&run)) + assert.Equal(t, test.discarded+1, atomic.LoadUint32(&executor.discarded)) + } + }) + } +} diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go new file mode 100644 index 0000000..f1de6c0 --- /dev/null +++ b/pkg/logger/logger.go @@ -0,0 +1,50 @@ +package logger + +import ( + "context" + "time" +) + +// A Logger represents a logger. +type Logger interface { + // Debug logs a message at debug level. + Debug(...any) + // Debugf logs a message at debug level. + Debugf(string, ...any) + // Debugv logs a message at debug level. + Debugv(any) + // Debugw logs a message at debug level. + Debugw(string, ...LogField) + // Error logs a message at error level. + Error(...any) + // Errorf logs a message at error level. + Errorf(string, ...any) + // Errorv logs a message at error level. + Errorv(any) + // Errorw logs a message at error level. + Errorw(string, ...LogField) + // Info logs a message at info level. + Info(...any) + // Infof logs a message at info level. + Infof(string, ...any) + // Infov logs a message at info level. + Infov(any) + // Infow logs a message at info level. + Infow(string, ...LogField) + // Slow logs a message at slow level. + Slow(...any) + // Slowf logs a message at slow level. + Slowf(string, ...any) + // Slowv logs a message at slow level. + Slowv(any) + // Sloww logs a message at slow level. + Sloww(string, ...LogField) + // WithCallerSkip returns a new logger with the given caller skip. + WithCallerSkip(skip int) Logger + // WithContext returns a new logger with the given context. + WithContext(ctx context.Context) Logger + // WithDuration returns a new logger with the given duration. + WithDuration(d time.Duration) Logger + // WithFields returns a new logger with the given fields. + WithFields(fields ...LogField) Logger +} diff --git a/pkg/logger/logs.go b/pkg/logger/logs.go new file mode 100644 index 0000000..7587093 --- /dev/null +++ b/pkg/logger/logs.go @@ -0,0 +1,578 @@ +package logger + +import ( + "fmt" + "io" + "log" + "os" + "path" + "reflect" + "runtime/debug" + "sync" + "sync/atomic" + "time" +) + +const callerDepth = 4 + +var ( + timeFormat = "2006-01-02T15:04:05.000Z07:00" + encoding uint32 = jsonEncodingType + // maxContentLength is used to truncate the log content, 0 for not truncating. + maxContentLength uint32 + // use uint32 for atomic operations + disableStat uint32 + logLevel uint32 + options logOptions + writer = new(atomicWriter) + setupOnce sync.Once +) + +type ( + // LogField is a key-value pair that will be added to the log entry. + LogField struct { + Key string + Value any + } + + // LogOption defines the method to customize the logging. + LogOption func(options *logOptions) + + logEntry map[string]any + + logOptions struct { + gzipEnabled bool + logStackCooldownMills int + keepDays int + maxBackups int + maxSize int + rotationRule string + } +) + +// AddWriter adds a new writer. +// If there is already a writer, the new writer will be added to the writer chain. +// For example, to write logs to both file and console, if there is already a file writer, +// ```go +// logx.AddWriter(logx.NewWriter(os.Stdout)) +// ``` +func AddWriter(w Writer) { + ow := Reset() + if ow == nil { + SetWriter(w) + } else { + // no need to check if the existing writer is a comboWriter, + // because it is not common to add more than one writer. + // even more than one writer, the behavior is the same. + SetWriter(comboWriter{ + writers: []Writer{ow, w}, + }) + } +} + +// Alert alerts v in alert level, and the message is written to error log. +func Alert(v string) { + getWriter().Alert(v) +} + +// Close closes the logging. +func Close() error { + if w := writer.Swap(nil); w != nil { + return w.(io.Closer).Close() + } + + return nil +} + +// Debug writes v into access log. +func Debug(v ...any) { + if shallLog(DebugLevel) { + writeDebug(fmt.Sprint(v...)) + } +} + +// Debugf writes v with format into access log. +func Debugf(format string, v ...any) { + if shallLog(DebugLevel) { + writeDebug(fmt.Sprintf(format, v...)) + } +} + +// Debugv writes v into access log with json content. +func Debugv(v any) { + if shallLog(DebugLevel) { + writeDebug(v) + } +} + +// Debugw writes msg along with fields into the access log. +func Debugw(msg string, fields ...LogField) { + if shallLog(DebugLevel) { + writeDebug(msg, fields...) + } +} + +// Disable disables the logging. +func Disable() { + atomic.StoreUint32(&logLevel, disableLevel) + writer.Store(nopWriter{}) +} + +// DisableStat disables the stat logs. +func DisableStat() { + atomic.StoreUint32(&disableStat, 1) +} + +// Error writes v into error log. +func Error(v ...any) { + if shallLog(ErrorLevel) { + writeError(fmt.Sprint(v...)) + } +} + +// Errorf writes v with format into error log. +func Errorf(format string, v ...any) { + if shallLog(ErrorLevel) { + writeError(fmt.Errorf(format, v...).Error()) + } +} + +// ErrorStack writes v along with call stack into error log. +func ErrorStack(v ...any) { + if shallLog(ErrorLevel) { + // there is newline in stack string + writeStack(fmt.Sprint(v...)) + } +} + +// ErrorStackf writes v along with call stack in format into error log. +func ErrorStackf(format string, v ...any) { + if shallLog(ErrorLevel) { + // there is newline in stack string + writeStack(fmt.Sprintf(format, v...)) + } +} + +// Errorv writes v into error log with json content. +// No call stack attached, because not elegant to pack the messages. +func Errorv(v any) { + if shallLog(ErrorLevel) { + writeError(v) + } +} + +// Errorw writes msg along with fields into the error log. +func Errorw(msg string, fields ...LogField) { + if shallLog(ErrorLevel) { + writeError(msg, fields...) + } +} + +// Field returns a LogField for the given key and value. +func Field(key string, value any) LogField { + switch val := value.(type) { + case error: + return LogField{Key: key, Value: encodeError(val)} + case []error: + var errs []string + for _, err := range val { + errs = append(errs, encodeError(err)) + } + return LogField{Key: key, Value: errs} + case time.Duration: + return LogField{Key: key, Value: fmt.Sprint(val)} + case []time.Duration: + var durs []string + for _, dur := range val { + durs = append(durs, fmt.Sprint(dur)) + } + return LogField{Key: key, Value: durs} + case []time.Time: + var times []string + for _, t := range val { + times = append(times, fmt.Sprint(t)) + } + return LogField{Key: key, Value: times} + case fmt.Stringer: + return LogField{Key: key, Value: encodeStringer(val)} + case []fmt.Stringer: + var strs []string + for _, str := range val { + strs = append(strs, encodeStringer(str)) + } + return LogField{Key: key, Value: strs} + default: + return LogField{Key: key, Value: val} + } +} + +// Info writes v into access log. +func Info(v ...any) { + if shallLog(InfoLevel) { + writeInfo(fmt.Sprint(v...)) + } +} + +// Infof writes v with format into access log. +func Infof(format string, v ...any) { + if shallLog(InfoLevel) { + writeInfo(fmt.Sprintf(format, v...)) + } +} + +// Infov writes v into access log with json content. +func Infov(v any) { + if shallLog(InfoLevel) { + writeInfo(v) + } +} + +// Infow writes msg along with fields into the access log. +func Infow(msg string, fields ...LogField) { + if shallLog(InfoLevel) { + writeInfo(msg, fields...) + } +} + +// Must checks if err is nil, otherwise logs the error and exits. +func Must(err error) { + if err == nil { + return + } + + msg := fmt.Sprintf("%+v\n\n%s", err.Error(), debug.Stack()) + log.Print(msg) + getWriter().Severe(msg) + + if ExitOnFatal.True() { + os.Exit(1) + } else { + panic(msg) + } +} + +// MustSetup sets up logging with given config c. It exits on error. +func MustSetup(c LogConf) { + Must(SetUp(c)) +} + +// Reset clears the writer and resets the log level. +func Reset() Writer { + return writer.Swap(nil) +} + +// SetLevel sets the logging level. It can be used to suppress some logs. +func SetLevel(level uint32) { + atomic.StoreUint32(&logLevel, level) +} + +// SetWriter sets the logging writer. It can be used to customize the logging. +func SetWriter(w Writer) { + if atomic.LoadUint32(&logLevel) != disableLevel { + writer.Store(w) + } +} + +// SetUp sets up the logx. +// If already set up, return nil. +// We allow SetUp to be called multiple times, because, for example, +// we need to allow different service frameworks to initialize logx respectively. +func SetUp(c LogConf) (err error) { + // Ignore the later SetUp calls. + // Because multiple services in one process might call SetUp respectively. + // Need to wait for the first caller to complete the execution. + setupOnce.Do(func() { + setupLogLevel(c) + + if !c.Stat { + DisableStat() + } + + if len(c.TimeFormat) > 0 { + timeFormat = c.TimeFormat + } + + if len(c.FileTimeFormat) > 0 { + fileTimeFormat = c.FileTimeFormat + } + + atomic.StoreUint32(&maxContentLength, c.MaxContentLength) + switch c.Encoding { + case plainEncoding: + atomic.StoreUint32(&encoding, plainEncodingType) + default: + atomic.StoreUint32(&encoding, jsonEncodingType) + } + + switch c.Mode { + case fileMode: + err = setupWithFiles(c) + case volumeMode: + err = setupWithVolume(c) + default: + setupWithConsole() + } + }) + + return +} + +// Severe writes v into severe log. +func Severe(v ...any) { + if shallLog(SevereLevel) { + writeSevere(fmt.Sprint(v...)) + } +} + +// Severef writes v with format into severe log. +func Severef(format string, v ...any) { + if shallLog(SevereLevel) { + writeSevere(fmt.Sprintf(format, v...)) + } +} + +// Slow writes v into slow log. +func Slow(v ...any) { + if shallLog(ErrorLevel) { + writeSlow(fmt.Sprint(v...)) + } +} + +// Slowf writes v with format into slow log. +func Slowf(format string, v ...any) { + if shallLog(ErrorLevel) { + writeSlow(fmt.Sprintf(format, v...)) + } +} + +// Slowv writes v into slow log with json content. +func Slowv(v any) { + if shallLog(ErrorLevel) { + writeSlow(v) + } +} + +// Sloww writes msg along with fields into slow log. +func Sloww(msg string, fields ...LogField) { + if shallLog(ErrorLevel) { + writeSlow(msg, fields...) + } +} + +// Stat writes v into stat log. +func Stat(v ...any) { + if shallLogStat() && shallLog(InfoLevel) { + writeStat(fmt.Sprint(v...)) + } +} + +// Statf writes v with format into stat log. +func Statf(format string, v ...any) { + if shallLogStat() && shallLog(InfoLevel) { + writeStat(fmt.Sprintf(format, v...)) + } +} + +// WithCooldownMillis customizes logging on writing call stack interval. +func WithCooldownMillis(millis int) LogOption { + return func(opts *logOptions) { + opts.logStackCooldownMills = millis + } +} + +// WithKeepDays customizes logging to keep logs with days. +func WithKeepDays(days int) LogOption { + return func(opts *logOptions) { + opts.keepDays = days + } +} + +// WithGzip customizes logging to automatically gzip the log files. +func WithGzip() LogOption { + return func(opts *logOptions) { + opts.gzipEnabled = true + } +} + +// WithMaxBackups customizes how many log files backups will be kept. +func WithMaxBackups(count int) LogOption { + return func(opts *logOptions) { + opts.maxBackups = count + } +} + +// WithMaxSize customizes how much space the writing log file can take up. +func WithMaxSize(size int) LogOption { + return func(opts *logOptions) { + opts.maxSize = size + } +} + +// WithRotation customizes which log rotation rule to use. +func WithRotation(r string) LogOption { + return func(opts *logOptions) { + opts.rotationRule = r + } +} + +func addCaller(fields ...LogField) []LogField { + return append(fields, Field(callerKey, getCaller(callerDepth))) +} + +func createOutput(path string) (io.WriteCloser, error) { + if len(path) == 0 { + return nil, ErrLogPathNotSet + } + + var rule RotateRule + switch options.rotationRule { + case sizeRotationRule: + rule = NewSizeLimitRotateRule(path, backupFileDelimiter, options.keepDays, options.maxSize, + options.maxBackups, options.gzipEnabled) + default: + rule = DefaultRotateRule(path, backupFileDelimiter, options.keepDays, options.gzipEnabled) + } + + return NewLogger(path, rule, options.gzipEnabled) +} + +func encodeError(err error) (ret string) { + return encodeWithRecover(err, func() string { + return err.Error() + }) +} + +func encodeStringer(v fmt.Stringer) (ret string) { + return encodeWithRecover(v, func() string { + return v.String() + }) +} + +func encodeWithRecover(arg any, fn func() string) (ret string) { + defer func() { + if err := recover(); err != nil { + if v := reflect.ValueOf(arg); v.Kind() == reflect.Ptr && v.IsNil() { + ret = nilAngleString + } else { + ret = fmt.Sprintf("panic: %v", err) + } + } + }() + + return fn() +} + +func getWriter() Writer { + w := writer.Load() + if w == nil { + w = writer.StoreIfNil(newConsoleWriter()) + } + + return w +} + +func handleOptions(opts []LogOption) { + for _, opt := range opts { + opt(&options) + } +} + +func setupLogLevel(c LogConf) { + switch c.Level { + case levelDebug: + SetLevel(DebugLevel) + case levelInfo: + SetLevel(InfoLevel) + case levelError: + SetLevel(ErrorLevel) + case levelSevere: + SetLevel(SevereLevel) + } +} + +func setupWithConsole() { + SetWriter(newConsoleWriter()) +} + +func setupWithFiles(c LogConf) error { + w, err := newFileWriter(c) + if err != nil { + return err + } + + SetWriter(w) + return nil +} + +func setupWithVolume(c LogConf) error { + if len(c.ServiceName) == 0 { + return ErrLogServiceNameNotSet + } + hostname, _ := os.Hostname() + c.Path = path.Join(c.Path, c.ServiceName, hostname) + return setupWithFiles(c) +} + +func shallLog(level uint32) bool { + return atomic.LoadUint32(&logLevel) <= level +} + +func shallLogStat() bool { + return atomic.LoadUint32(&disableStat) == 0 +} + +// writeDebug writes v into debug log. +// Not checking shallLog here is for performance consideration. +// If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled. +// The caller should check shallLog before calling this function. +func writeDebug(val any, fields ...LogField) { + getWriter().Debug(val, addCaller(fields...)...) +} + +// writeError writes v into the error log. +// Not checking shallLog here is for performance consideration. +// If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled. +// The caller should check shallLog before calling this function. +func writeError(val any, fields ...LogField) { + getWriter().Error(val, addCaller(fields...)...) +} + +// writeInfo writes v into info log. +// Not checking shallLog here is for performance consideration. +// If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled. +// The caller should check shallLog before calling this function. +func writeInfo(val any, fields ...LogField) { + getWriter().Info(val, addCaller(fields...)...) +} + +// writeSevere writes v into severe log. +// Not checking shallLog here is for performance consideration. +// If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled. +// The caller should check shallLog before calling this function. +func writeSevere(msg string) { + getWriter().Severe(fmt.Sprintf("%s\n%s", msg, string(debug.Stack()))) +} + +// writeSlow writes v into slow log. +// Not checking shallLog here is for performance consideration. +// If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled. +// The caller should check shallLog before calling this function. +func writeSlow(val any, fields ...LogField) { + getWriter().Slow(val, addCaller(fields...)...) +} + +// writeStack writes v into stack log. +// Not checking shallLog here is for performance consideration. +// If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled. +// The caller should check shallLog before calling this function. +func writeStack(msg string) { + getWriter().Stack(fmt.Sprintf("%s\n%s", msg, string(debug.Stack()))) +} + +// writeStat writes v into the stat log. +// Not checking shallLog here is for performance consideration. +// If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled. +// The caller should check shallLog before calling this function. +func writeStat(msg string) { + getWriter().Stat(msg, addCaller()...) +} diff --git a/pkg/logger/logs_test.go b/pkg/logger/logs_test.go new file mode 100644 index 0000000..c67bd79 --- /dev/null +++ b/pkg/logger/logs_test.go @@ -0,0 +1,931 @@ +package logger + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "log" + "os" + "reflect" + "runtime" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var ( + s = []byte("Sending #11 notification (id: 1451875113812010473) in #1 connection") + pool = make(chan []byte, 1) + _ Writer = (*mockWriter)(nil) +) + +func init() { + ExitOnFatal.Set(false) +} + +type mockWriter struct { + lock sync.Mutex + builder strings.Builder +} + +func (mw *mockWriter) Alert(v any) { + mw.lock.Lock() + defer mw.lock.Unlock() + output(&mw.builder, levelAlert, v) +} + +func (mw *mockWriter) Debug(v any, fields ...LogField) { + mw.lock.Lock() + defer mw.lock.Unlock() + output(&mw.builder, levelDebug, v, fields...) +} + +func (mw *mockWriter) Error(v any, fields ...LogField) { + mw.lock.Lock() + defer mw.lock.Unlock() + output(&mw.builder, levelError, v, fields...) +} + +func (mw *mockWriter) Info(v any, fields ...LogField) { + mw.lock.Lock() + defer mw.lock.Unlock() + output(&mw.builder, levelInfo, v, fields...) +} + +func (mw *mockWriter) Severe(v any) { + mw.lock.Lock() + defer mw.lock.Unlock() + output(&mw.builder, levelSevere, v) +} + +func (mw *mockWriter) Slow(v any, fields ...LogField) { + mw.lock.Lock() + defer mw.lock.Unlock() + output(&mw.builder, levelSlow, v, fields...) +} + +func (mw *mockWriter) Stack(v any) { + mw.lock.Lock() + defer mw.lock.Unlock() + output(&mw.builder, levelError, v) +} + +func (mw *mockWriter) Stat(v any, fields ...LogField) { + mw.lock.Lock() + defer mw.lock.Unlock() + output(&mw.builder, levelStat, v, fields...) +} + +func (mw *mockWriter) Close() error { + return nil +} + +func (mw *mockWriter) Contains(text string) bool { + mw.lock.Lock() + defer mw.lock.Unlock() + return strings.Contains(mw.builder.String(), text) +} + +func (mw *mockWriter) Reset() { + mw.lock.Lock() + defer mw.lock.Unlock() + mw.builder.Reset() +} + +func (mw *mockWriter) String() string { + mw.lock.Lock() + defer mw.lock.Unlock() + return mw.builder.String() +} + +func TestField(t *testing.T) { + tests := []struct { + name string + f LogField + want map[string]any + }{ + { + name: "error", + f: Field("foo", errors.New("bar")), + want: map[string]any{ + "foo": "bar", + }, + }, + { + name: "errors", + f: Field("foo", []error{errors.New("bar"), errors.New("baz")}), + want: map[string]any{ + "foo": []any{"bar", "baz"}, + }, + }, + { + name: "strings", + f: Field("foo", []string{"bar", "baz"}), + want: map[string]any{ + "foo": []any{"bar", "baz"}, + }, + }, + { + name: "duration", + f: Field("foo", time.Second), + want: map[string]any{ + "foo": "1s", + }, + }, + { + name: "durations", + f: Field("foo", []time.Duration{time.Second, 2 * time.Second}), + want: map[string]any{ + "foo": []any{"1s", "2s"}, + }, + }, + { + name: "times", + f: Field("foo", []time.Time{ + time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), + time.Date(2020, time.January, 2, 0, 0, 0, 0, time.UTC), + }), + want: map[string]any{ + "foo": []any{"2020-01-01 00:00:00 +0000 UTC", "2020-01-02 00:00:00 +0000 UTC"}, + }, + }, + { + name: "stringer", + f: Field("foo", ValStringer{val: "bar"}), + want: map[string]any{ + "foo": "bar", + }, + }, + { + name: "stringers", + f: Field("foo", []fmt.Stringer{ValStringer{val: "bar"}, ValStringer{val: "baz"}}), + want: map[string]any{ + "foo": []any{"bar", "baz"}, + }, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + Infow("foo", test.f) + validateFields(t, w.String(), test.want) + }) + } +} + +func TestFileLineFileMode(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + file, line := getFileLine() + Error("anything") + assert.True(t, w.Contains(fmt.Sprintf("%s:%d", file, line+1))) + + file, line = getFileLine() + Errorf("anything %s", "format") + assert.True(t, w.Contains(fmt.Sprintf("%s:%d", file, line+1))) +} + +func TestFileLineConsoleMode(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + file, line := getFileLine() + Error("anything") + assert.True(t, w.Contains(fmt.Sprintf("%s:%d", file, line+1))) + + w.Reset() + file, line = getFileLine() + Errorf("anything %s", "format") + assert.True(t, w.Contains(fmt.Sprintf("%s:%d", file, line+1))) +} + +func TestMust(t *testing.T) { + assert.Panics(t, func() { + Must(errors.New("foo")) + }) +} + +func TestStructedLogAlert(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLog(t, levelAlert, w, func(v ...any) { + Alert(fmt.Sprint(v...)) + }) +} + +func TestStructedLogDebug(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLog(t, levelDebug, w, func(v ...any) { + Debug(v...) + }) +} + +func TestStructedLogDebugf(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLog(t, levelDebug, w, func(v ...any) { + Debugf(fmt.Sprint(v...)) + }) +} + +func TestStructedLogDebugv(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLog(t, levelDebug, w, func(v ...any) { + Debugv(fmt.Sprint(v...)) + }) +} + +func TestStructedLogDebugw(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLog(t, levelDebug, w, func(v ...any) { + Debugw(fmt.Sprint(v...), Field("foo", time.Second)) + }) +} + +func TestStructedLogError(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLog(t, levelError, w, func(v ...any) { + Error(v...) + }) +} + +func TestStructedLogErrorf(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLog(t, levelError, w, func(v ...any) { + Errorf("%s", fmt.Sprint(v...)) + }) +} + +func TestStructedLogErrorv(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLog(t, levelError, w, func(v ...any) { + Errorv(fmt.Sprint(v...)) + }) +} + +func TestStructedLogErrorw(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLog(t, levelError, w, func(v ...any) { + Errorw(fmt.Sprint(v...), Field("foo", "bar")) + }) +} + +func TestStructedLogInfo(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLog(t, levelInfo, w, func(v ...any) { + Info(v...) + }) +} + +func TestStructedLogInfof(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLog(t, levelInfo, w, func(v ...any) { + Infof("%s", fmt.Sprint(v...)) + }) +} + +func TestStructedLogInfov(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLog(t, levelInfo, w, func(v ...any) { + Infov(fmt.Sprint(v...)) + }) +} + +func TestStructedLogInfow(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLog(t, levelInfo, w, func(v ...any) { + Infow(fmt.Sprint(v...), Field("foo", "bar")) + }) +} + +func TestStructedLogFieldNil(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + assert.NotPanics(t, func() { + var s *string + Infow("test", Field("bb", s)) + var d *nilStringer + Infow("test", Field("bb", d)) + var e *nilError + Errorw("test", Field("bb", e)) + }) + assert.NotPanics(t, func() { + var p panicStringer + Infow("test", Field("bb", p)) + var ps innerPanicStringer + Infow("test", Field("bb", ps)) + }) +} + +func TestStructedLogInfoConsoleAny(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLogConsole(t, w, func(v ...any) { + old := atomic.LoadUint32(&encoding) + atomic.StoreUint32(&encoding, plainEncodingType) + defer func() { + atomic.StoreUint32(&encoding, old) + }() + + Infov(v) + }) +} + +func TestStructedLogInfoConsoleAnyString(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLogConsole(t, w, func(v ...any) { + old := atomic.LoadUint32(&encoding) + atomic.StoreUint32(&encoding, plainEncodingType) + defer func() { + atomic.StoreUint32(&encoding, old) + }() + + Infov(fmt.Sprint(v...)) + }) +} + +func TestStructedLogInfoConsoleAnyError(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLogConsole(t, w, func(v ...any) { + old := atomic.LoadUint32(&encoding) + atomic.StoreUint32(&encoding, plainEncodingType) + defer func() { + atomic.StoreUint32(&encoding, old) + }() + + Infov(errors.New(fmt.Sprint(v...))) + }) +} + +func TestStructedLogInfoConsoleAnyStringer(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLogConsole(t, w, func(v ...any) { + old := atomic.LoadUint32(&encoding) + atomic.StoreUint32(&encoding, plainEncodingType) + defer func() { + atomic.StoreUint32(&encoding, old) + }() + + Infov(ValStringer{ + val: fmt.Sprint(v...), + }) + }) +} + +func TestStructedLogInfoConsoleText(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLogConsole(t, w, func(v ...any) { + old := atomic.LoadUint32(&encoding) + atomic.StoreUint32(&encoding, plainEncodingType) + defer func() { + atomic.StoreUint32(&encoding, old) + }() + + Info(fmt.Sprint(v...)) + }) +} + +func TestStructedLogSlow(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLog(t, levelSlow, w, func(v ...any) { + Slow(v...) + }) +} + +func TestStructedLogSlowf(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLog(t, levelSlow, w, func(v ...any) { + Slowf(fmt.Sprint(v...)) + }) +} + +func TestStructedLogSlowv(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLog(t, levelSlow, w, func(v ...any) { + Slowv(fmt.Sprint(v...)) + }) +} + +func TestStructedLogSloww(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLog(t, levelSlow, w, func(v ...any) { + Sloww(fmt.Sprint(v...), Field("foo", time.Second)) + }) +} + +func TestStructedLogStat(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLog(t, levelStat, w, func(v ...any) { + Stat(v...) + }) +} + +func TestStructedLogStatf(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLog(t, levelStat, w, func(v ...any) { + Statf(fmt.Sprint(v...)) + }) +} + +func TestStructedLogSevere(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLog(t, levelSevere, w, func(v ...any) { + Severe(v...) + }) +} + +func TestStructedLogSeveref(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + doTestStructedLog(t, levelSevere, w, func(v ...any) { + Severef(fmt.Sprint(v...)) + }) +} + +func TestStructedLogWithDuration(t *testing.T) { + const message = "hello there" + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + WithDuration(time.Second).Info(message) + var entry map[string]any + if err := json.Unmarshal([]byte(w.String()), &entry); err != nil { + t.Error(err) + } + assert.Equal(t, levelInfo, entry[levelKey]) + assert.Equal(t, message, entry[contentKey]) + assert.Equal(t, "1000.0ms", entry[durationKey]) +} + +func TestSetLevel(t *testing.T) { + SetLevel(ErrorLevel) + const message = "hello there" + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + Info(message) + assert.Equal(t, 0, w.builder.Len()) +} + +func TestSetLevelTwiceWithMode(t *testing.T) { + testModes := []string{ + "console", + "volumn", + "mode", + } + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + for _, mode := range testModes { + testSetLevelTwiceWithMode(t, mode, w) + } +} + +func TestSetLevelWithDuration(t *testing.T) { + SetLevel(ErrorLevel) + const message = "hello there" + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + WithDuration(time.Second).Info(message) + assert.Equal(t, 0, w.builder.Len()) +} + +func TestErrorfWithWrappedError(t *testing.T) { + SetLevel(ErrorLevel) + const message = "there" + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + Errorf("hello %s", errors.New(message)) + assert.True(t, strings.Contains(w.String(), "hello there")) +} + +func TestMustNil(t *testing.T) { + Must(nil) +} + +func TestSetup(t *testing.T) { + defer func() { + SetLevel(InfoLevel) + atomic.StoreUint32(&encoding, jsonEncodingType) + }() + + setupOnce = sync.Once{} + MustSetup(LogConf{ + ServiceName: "any", + Mode: "console", + Encoding: "json", + TimeFormat: timeFormat, + }) + setupOnce = sync.Once{} + MustSetup(LogConf{ + ServiceName: "any", + Mode: "console", + TimeFormat: timeFormat, + }) + setupOnce = sync.Once{} + MustSetup(LogConf{ + ServiceName: "any", + Mode: "file", + Path: os.TempDir(), + }) + setupOnce = sync.Once{} + MustSetup(LogConf{ + ServiceName: "any", + Mode: "volume", + Path: os.TempDir(), + }) + setupOnce = sync.Once{} + MustSetup(LogConf{ + ServiceName: "any", + Mode: "console", + TimeFormat: timeFormat, + }) + setupOnce = sync.Once{} + MustSetup(LogConf{ + ServiceName: "any", + Mode: "console", + Encoding: plainEncoding, + }) + + defer os.RemoveAll("CD01CB7D-2705-4F3F-889E-86219BF56F10") + assert.NotNil(t, setupWithVolume(LogConf{})) + assert.Nil(t, setupWithVolume(LogConf{ + ServiceName: "CD01CB7D-2705-4F3F-889E-86219BF56F10", + })) + assert.Nil(t, setupWithVolume(LogConf{ + ServiceName: "CD01CB7D-2705-4F3F-889E-86219BF56F10", + Rotation: sizeRotationRule, + })) + assert.NotNil(t, setupWithFiles(LogConf{})) + assert.Nil(t, setupWithFiles(LogConf{ + ServiceName: "any", + Path: os.TempDir(), + Compress: true, + KeepDays: 1, + MaxBackups: 3, + MaxSize: 1024 * 1024, + })) + setupLogLevel(LogConf{ + Level: levelInfo, + }) + setupLogLevel(LogConf{ + Level: levelError, + }) + setupLogLevel(LogConf{ + Level: levelSevere, + }) + _, err := createOutput("") + assert.NotNil(t, err) + Disable() + SetLevel(InfoLevel) + atomic.StoreUint32(&encoding, jsonEncodingType) +} + +func TestDisable(t *testing.T) { + Disable() + defer func() { + SetLevel(InfoLevel) + atomic.StoreUint32(&encoding, jsonEncodingType) + }() + + var opt logOptions + WithKeepDays(1)(&opt) + WithGzip()(&opt) + WithMaxBackups(1)(&opt) + WithMaxSize(1024)(&opt) + assert.Nil(t, Close()) + assert.Nil(t, Close()) + assert.Equal(t, uint32(disableLevel), atomic.LoadUint32(&logLevel)) +} + +func TestDisableStat(t *testing.T) { + DisableStat() + + const message = "hello there" + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + Stat(message) + assert.Equal(t, 0, w.builder.Len()) +} + +func TestAddWriter(t *testing.T) { + const message = "hello there" + w := new(mockWriter) + AddWriter(w) + w1 := new(mockWriter) + AddWriter(w1) + Error(message) + assert.Contains(t, w.String(), message) + assert.Contains(t, w1.String(), message) +} + +func TestSetWriter(t *testing.T) { + atomic.StoreUint32(&logLevel, 0) + Reset() + SetWriter(nopWriter{}) + assert.NotNil(t, writer.Load()) + assert.True(t, writer.Load() == nopWriter{}) + mocked := new(mockWriter) + SetWriter(mocked) + assert.Equal(t, mocked, writer.Load()) +} + +func TestWithGzip(t *testing.T) { + fn := WithGzip() + var opt logOptions + fn(&opt) + assert.True(t, opt.gzipEnabled) +} + +func TestWithKeepDays(t *testing.T) { + fn := WithKeepDays(1) + var opt logOptions + fn(&opt) + assert.Equal(t, 1, opt.keepDays) +} + +func BenchmarkCopyByteSliceAppend(b *testing.B) { + for i := 0; i < b.N; i++ { + var buf []byte + buf = append(buf, getTimestamp()...) + buf = append(buf, ' ') + buf = append(buf, s...) + _ = buf + } +} + +func BenchmarkCopyByteSliceAllocExactly(b *testing.B) { + for i := 0; i < b.N; i++ { + now := []byte(getTimestamp()) + buf := make([]byte, len(now)+1+len(s)) + n := copy(buf, now) + buf[n] = ' ' + copy(buf[n+1:], s) + } +} + +func BenchmarkCopyByteSlice(b *testing.B) { + var buf []byte + for i := 0; i < b.N; i++ { + buf = make([]byte, len(s)) + copy(buf, s) + } + fmt.Fprint(io.Discard, buf) +} + +func BenchmarkCopyOnWriteByteSlice(b *testing.B) { + var buf []byte + for i := 0; i < b.N; i++ { + size := len(s) + buf = s[:size:size] + } + fmt.Fprint(io.Discard, buf) +} + +func BenchmarkCacheByteSlice(b *testing.B) { + for i := 0; i < b.N; i++ { + dup := fetch() + copy(dup, s) + put(dup) + } +} + +func BenchmarkLogs(b *testing.B) { + b.ReportAllocs() + + log.SetOutput(io.Discard) + for i := 0; i < b.N; i++ { + Info(i) + } +} + +func fetch() []byte { + select { + case b := <-pool: + return b + default: + } + return make([]byte, 4096) +} + +func getFileLine() (string, int) { + _, file, line, _ := runtime.Caller(1) + short := file + + for i := len(file) - 1; i > 0; i-- { + if file[i] == '/' { + short = file[i+1:] + break + } + } + + return short, line +} + +func put(b []byte) { + select { + case pool <- b: + default: + } +} + +func doTestStructedLog(t *testing.T, level string, w *mockWriter, write func(...any)) { + const message = "hello there" + write(message) + + var entry map[string]any + if err := json.Unmarshal([]byte(w.String()), &entry); err != nil { + t.Error(err) + } + + assert.Equal(t, level, entry[levelKey]) + val, ok := entry[contentKey] + assert.True(t, ok) + assert.True(t, strings.Contains(val.(string), message)) +} + +func doTestStructedLogConsole(t *testing.T, w *mockWriter, write func(...any)) { + const message = "hello there" + write(message) + assert.True(t, strings.Contains(w.String(), message)) +} + +func testSetLevelTwiceWithMode(t *testing.T, mode string, w *mockWriter) { + writer.Store(nil) + _ = SetUp(LogConf{ + Mode: mode, + Level: "debug", + Path: "/dev/null", + Encoding: plainEncoding, + Stat: false, + TimeFormat: time.RFC3339, + FileTimeFormat: time.DateTime, + }) + _ = SetUp(LogConf{ + Mode: mode, + Level: "info", + Path: "/dev/null", + }) + const message = "hello there" + Info(message) + assert.Equal(t, 0, w.builder.Len()) + Infof(message) + assert.Equal(t, 0, w.builder.Len()) + ErrorStack(message) + assert.Equal(t, 0, w.builder.Len()) + ErrorStackf(message) + assert.Equal(t, 0, w.builder.Len()) +} + +type ValStringer struct { + val string +} + +func (v ValStringer) String() string { + return v.val +} + +func validateFields(t *testing.T, content string, fields map[string]any) { + var m map[string]any + if err := json.Unmarshal([]byte(content), &m); err != nil { + t.Error(err) + } + + for k, v := range fields { + if reflect.TypeOf(v).Kind() == reflect.Slice { + assert.EqualValues(t, v, m[k]) + } else { + assert.Equal(t, v, m[k], content) + } + } +} + +type nilError struct { + Name string +} + +func (e *nilError) Error() string { + return e.Name +} + +type nilStringer struct { + Name string +} + +func (s *nilStringer) String() string { + return s.Name +} + +type innerPanicStringer struct { + Inner *struct { + Name string + } +} + +func (s innerPanicStringer) String() string { + return s.Inner.Name +} + +type panicStringer struct { +} + +func (s panicStringer) String() string { + panic("panic") +} diff --git a/pkg/logger/logtest/logtest.go b/pkg/logger/logtest/logtest.go new file mode 100644 index 0000000..779ab22 --- /dev/null +++ b/pkg/logger/logtest/logtest.go @@ -0,0 +1,84 @@ +package logtest + +import ( + "bytes" + "encoding/json" + "io" + "testing" + + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +type Buffer struct { + buf *bytes.Buffer + t *testing.T +} + +func Discard(t *testing.T) { + prev := logger.Reset() + logger.SetWriter(logger.NewWriter(io.Discard)) + + t.Cleanup(func() { + logger.SetWriter(prev) + }) +} + +func NewCollector(t *testing.T) *Buffer { + var buf bytes.Buffer + writer := logger.NewWriter(&buf) + prev := logger.Reset() + logger.SetWriter(writer) + + t.Cleanup(func() { + logger.SetWriter(prev) + }) + + return &Buffer{ + buf: &buf, + t: t, + } +} + +func (b *Buffer) Bytes() []byte { + return b.buf.Bytes() +} + +func (b *Buffer) Content() string { + var m map[string]interface{} + if err := json.Unmarshal(b.buf.Bytes(), &m); err != nil { + return "" + } + + content, ok := m["content"] + if !ok { + return "" + } + + switch val := content.(type) { + case string: + return val + default: + // err is impossible to be not nil, unmarshaled from b.buf.Bytes() + bs, _ := json.Marshal(content) + return string(bs) + } +} + +func (b *Buffer) Reset() { + b.buf.Reset() +} + +func (b *Buffer) String() string { + return b.buf.String() +} + +func PanicOnFatal(t *testing.T) { + ok := logger.ExitOnFatal.CompareAndSwap(true, false) + if !ok { + return + } + + t.Cleanup(func() { + logger.ExitOnFatal.CompareAndSwap(false, true) + }) +} diff --git a/pkg/logger/logtest/logtest_test.go b/pkg/logger/logtest/logtest_test.go new file mode 100644 index 0000000..e98aed3 --- /dev/null +++ b/pkg/logger/logtest/logtest_test.go @@ -0,0 +1,44 @@ +package logtest + +import ( + "errors" + "testing" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/stretchr/testify/assert" +) + +func TestCollector(t *testing.T) { + const input = "hello" + c := NewCollector(t) + logger.Info(input) + assert.Equal(t, input, c.Content()) + assert.Contains(t, c.String(), input) + c.Reset() + assert.Empty(t, c.Bytes()) +} + +func TestPanicOnFatal(t *testing.T) { + const input = "hello" + Discard(t) + logger.Info(input) + + PanicOnFatal(t) + PanicOnFatal(t) + assert.Panics(t, func() { + logger.Must(errors.New("foo")) + }) +} + +func TestCollectorContent(t *testing.T) { + const input = "hello" + c := NewCollector(t) + c.buf.WriteString(input) + assert.Empty(t, c.Content()) + c.Reset() + c.buf.WriteString(`{}`) + assert.Empty(t, c.Content()) + c.Reset() + c.buf.WriteString(`{"content":1}`) + assert.Equal(t, "1", c.Content()) +} diff --git a/pkg/logger/logwriter.go b/pkg/logger/logwriter.go new file mode 100644 index 0000000..3fcd734 --- /dev/null +++ b/pkg/logger/logwriter.go @@ -0,0 +1,22 @@ +package logger + +import "log" + +type logWriter struct { + logger *log.Logger +} + +func newLogWriter(logger *log.Logger) logWriter { + return logWriter{ + logger: logger, + } +} + +func (lw logWriter) Close() error { + return nil +} + +func (lw logWriter) Write(data []byte) (int, error) { + lw.logger.Print(string(data)) + return len(data), nil +} diff --git a/pkg/logger/readme-cn.md b/pkg/logger/readme-cn.md new file mode 100644 index 0000000..94104e9 --- /dev/null +++ b/pkg/logger/readme-cn.md @@ -0,0 +1,144 @@ +## logger 配置 + +```go +type LogConf struct { + ServiceName string + Mode string + Encoding string + TimeFormat string + Path string + Level string + Compress bool + KeepDays int + StackCooldownMillis int + MaxBackups int + MaxSize int + Rotation string +} +``` + +- `ServiceName`:设置服务名称,可选。在 `volume` 模式下,该名称用于生成日志文件。 +- `Mode`:输出日志的模式,默认是 `console` + - `console` 模式将日志写到 `stdout/stderr` + - `file` 模式将日志写到 `Path` 指定目录的文件中 + - `volume` 模式在 docker 中使用,将日志写入挂载的卷中 +- `Encoding`: 指示如何对日志进行编码,默认是 `json` + - `json`模式以 json 格式写日志 + - `plain`模式用纯文本写日志,并带有终端颜色显示 +- `TimeFormat`:自定义时间格式,可选。默认是 `2006-01-02T15:04:05.000Z07:00` +- `Path`:设置日志路径,默认为 `logs` +- `Level`: 用于过滤日志的日志级别。默认为 `info` + - `info`,所有日志都被写入 + - `error`, `info` 的日志被丢弃 + - `severe`, `info` 和 `error` 日志被丢弃,只有 `severe` 日志被写入 +- `Compress`: 是否压缩日志文件,只在 `file` 模式下工作 +- `KeepDays`:日志文件被保留多少天,在给定的天数之后,过期的文件将被自动删除。对 `console` 模式没有影响 +- `StackCooldownMillis`:多少毫秒后再次写入堆栈跟踪。用来避免堆栈跟踪日志过多 +- `MaxBackups`: 多少个日志文件备份将被保存。0代表所有备份都被保存。当`Rotation`被设置为`size`时才会起作用。注意:`KeepDays`选项的优先级会比`MaxBackups`高,即使`MaxBackups`被设置为0,当达到`KeepDays`上限时备份文件同样会被删除。 +- `MaxSize`: 当前被写入的日志文件最大可占用多少空间。0代表没有上限。单位为`MB`。当`Rotation`被设置为`size`时才会起作用。 +- `Rotation`: 日志轮转策略类型。默认为`daily`(按天轮转)。 + - `daily` 按天轮转。 + - `size` 按日志大小轮转。 + + +## 打印日志方法 + +```go +type Logger interface { + // Error logs a message at error level. + Error(...any) + // Errorf logs a message at error level. + Errorf(string, ...any) + // Errorv logs a message at error level. + Errorv(any) + // Errorw logs a message at error level. + Errorw(string, ...LogField) + // Info logs a message at info level. + Info(...any) + // Infof logs a message at info level. + Infof(string, ...any) + // Infov logs a message at info level. + Infov(any) + // Infow logs a message at info level. + Infow(string, ...LogField) + // Slow logs a message at slow level. + Slow(...any) + // Slowf logs a message at slow level. + Slowf(string, ...any) + // Slowv logs a message at slow level. + Slowv(any) + // Sloww logs a message at slow level. + Sloww(string, ...LogField) + // WithContext returns a new logger with the given context. + WithContext(context.Context) Logger + // WithDuration returns a new logger with the given duration. + WithDuration(time.Duration) Logger +} +``` + +- `Error`, `Info`, `Slow`: 将任何类型的信息写进日志,使用 `fmt.Sprint(...)` 来转换为 `string` +- `Errorf`, `Infof`, `Slowf`: 将指定格式的信息写入日志 +- `Errorv`, `Infov`, `Slowv`: 将任何类型的信息写入日志,用 `json marshal` 编码 +- `Errorw`, `Infow`, `Sloww`: 写日志,并带上给定的 `key:value` 字段 +- `WithContext`:将给定的 ctx 注入日志信息,例如用于记录 `trace-id`和`span-id` +- `WithDuration`: 将指定的时间写入日志信息中,字段名为 `duration` + +## 将日志写到指定的存储 + +`logger`定义了两个接口,方便自定义 `logger`,将日志写入任何存储。 + +- `logger.NewWriter(w io.Writer)` +- `logger.SetWriter(write logger.Writer)` + +## 过滤敏感字段 + +如果我们需要防止 `password` 字段被记录下来,我们可以像下面这样实现。 + +```go +type ( + Message struct { + Name string + Password string + Message string + } + + SensitiveLogger struct { + logger.Writer + } +) + +func NewSensitiveLogger(writer logger.Writer) *SensitiveLogger { + return &SensitiveLogger{ + Writer: writer, + } +} + +func (l *SensitiveLogger) Info(msg any, fields ...logger.LogField) { + if m, ok := msg.(Message); ok { + l.Writer.Info(Message{ + Name: m.Name, + Password: "******", + Message: m.Message, + }, fields...) + } else { + l.Writer.Info(msg, fields...) + } +} + +func main() { + // setup logger to make sure originalWriter not nil, + // the injected writer is only for filtering, like a middleware. + + originalWriter := logger.Reset() + writer := NewSensitiveLogger(originalWriter) + logger.SetWriter(writer) + + logger.Infov(Message{ + Name: "foo", + Password: "shouldNotAppear", + Message: "bar", + }) + + // more code +} +``` \ No newline at end of file diff --git a/pkg/logger/readme.md b/pkg/logger/readme.md new file mode 100644 index 0000000..8e2277c --- /dev/null +++ b/pkg/logger/readme.md @@ -0,0 +1,144 @@ + +## logger configurations + +```go +type LogConf struct { + ServiceName string + Mode string + Encoding string + TimeFormat string + Path string + Level string + Compress bool + KeepDays int + StackCooldownMillis int + MaxBackups int + MaxSize int + Rotation string +} +``` + +- `ServiceName`: set the service name, optional. on `volume` mode, the name is used to generate the log files. Within `rest/zrpc` services, the name will be set to the name of `rest` or `zrpc` automatically. +- `Mode`: the mode to output the logs, default is `console`. + - `console` mode writes the logs to `stdout/stderr`. + - `file` mode writes the logs to the files specified by `Path`. + - `volume` mode is used in docker, to write logs into mounted volumes. +- `Encoding`: indicates how to encode the logs, default is `json`. + - `json` mode writes the logs in json format. + - `plain` mode writes the logs with plain text, with terminal color enabled. +- `TimeFormat`: customize the time format, optional. Default is `2006-01-02T15:04:05.000Z07:00`. +- `Path`: set the log path, default to `logs`. +- `Level`: the logging level to filter logs. Default is `info`. + - `info`, all logs are written. + - `error`, `info` logs are suppressed. + - `severe`, `info` and `error` logs are suppressed, only `severe` logs are written. +- `Compress`: whether or not to compress log files, only works with `file` mode. +- `KeepDays`: how many days that the log files are kept, after the given days, the outdated files will be deleted automatically. It has no effect on `console` mode. +- `StackCooldownMillis`: how many milliseconds to rewrite stacktrace again. It’s used to avoid stacktrace flooding. +- `MaxBackups`: represents how many backup log files will be kept. 0 means all files will be kept forever. Only take effect when `Rotation` is `size`. NOTE: the level of option `KeepDays` will be higher. Even though `MaxBackups` sets 0, log files will still be removed if the `KeepDays` limitation is reached. +- `MaxSize`: represents how much space the writing log file takes up. 0 means no limit. The unit is `MB`. Only take effect when `Rotation` is `size`. +- `Rotation`: represents the type of log rotation rule. Default is `daily`. + - `daily` rotate the logs by day. + - `size` rotate the logs by size of logs. + +## Logging methods + +```go +type Logger interface { + // Error logs a message at error level. + Error(...any) + // Errorf logs a message at error level. + Errorf(string, ...any) + // Errorv logs a message at error level. + Errorv(any) + // Errorw logs a message at error level. + Errorw(string, ...LogField) + // Info logs a message at info level. + Info(...any) + // Infof logs a message at info level. + Infof(string, ...any) + // Infov logs a message at info level. + Infov(any) + // Infow logs a message at info level. + Infow(string, ...LogField) + // Slow logs a message at slow level. + Slow(...any) + // Slowf logs a message at slow level. + Slowf(string, ...any) + // Slowv logs a message at slow level. + Slowv(any) + // Sloww logs a message at slow level. + Sloww(string, ...LogField) + // WithContext returns a new logger with the given context. + WithContext(context.Context) Logger + // WithDuration returns a new logger with the given duration. + WithDuration(time.Duration) Logger +} +``` + +- `Error`, `Info`, `Slow`: write any kind of messages into logs, with like `fmt.Sprint(…)`. +- `Errorf`, `Infof`, `Slowf`: write messages with given format into logs. +- `Errorv`, `Infov`, `Slowv`: write any kind of messages into logs, with json marshalling to encode them. +- `Errorw`, `Infow`, `Sloww`: write the string message with given `key:value` fields. +- `WithContext`: inject the given ctx into the log messages, typically used to log `trace-id` and `span-id`. +- `WithDuration`: write elapsed duration into the log messages, with key `duration`. + +## Write the logs to specific stores + +`logger` defined two interfaces to let you customize `logger` to write logs into any stores. + +- `logger.NewWriter(w io.Writer)` +- `logger.SetWriter(writer logx.Writer)` + +## Filtering sensitive fields + +If we need to prevent the `password` fields from logging, we can do it like below: + +```go +type ( + Message struct { + Name string + Password string + Message string + } + + SensitiveLogger struct { + logger.Writer + } +) + +func NewSensitiveLogger(writer logger.Writer) *SensitiveLogger { + return &SensitiveLogger{ + Writer: writer, + } +} + +func (l *SensitiveLogger) Info(msg any, fields ...logx.LogField) { + if m, ok := msg.(Message); ok { + l.Writer.Info(Message{ + Name: m.Name, + Password: "******", + Message: m.Message, + }, fields...) + } else { + l.Writer.Info(msg, fields...) + } +} + +func main() { + // setup logx to make sure originalWriter not nil, + // the injected writer is only for filtering, like a middleware. + + originalWriter := logger.Reset() + writer := NewSensitiveLogger(originalWriter) + logger.SetWriter(writer) + + logger.Infov(Message{ + Name: "foo", + Password: "shouldNotAppear", + Message: "bar", + }) + + // more code +} +``` \ No newline at end of file diff --git a/pkg/logger/richlogger.go b/pkg/logger/richlogger.go new file mode 100644 index 0000000..a949510 --- /dev/null +++ b/pkg/logger/richlogger.go @@ -0,0 +1,234 @@ +package logger + +import ( + "context" + "fmt" + "time" + + "github.com/perfect-panel/ppanel-server/internal/trace" + + "github.com/perfect-panel/ppanel-server/pkg/timex" +) + +// WithCallerSkip returns a Logger with given caller skip. +func WithCallerSkip(skip int) Logger { + if skip <= 0 { + return new(richLogger) + } + + return &richLogger{ + callerSkip: skip, + } +} + +// WithContext sets ctx to log, for keeping tracing information. +func WithContext(ctx context.Context) Logger { + return &richLogger{ + ctx: ctx, + } +} + +// WithDuration returns a Logger with given duration. +func WithDuration(d time.Duration) Logger { + return &richLogger{ + fields: []LogField{Field(durationKey, timex.ReprOfDuration(d))}, + } +} + +type richLogger struct { + ctx context.Context + callerSkip int + fields []LogField +} + +func (l *richLogger) Debug(v ...any) { + if shallLog(DebugLevel) { + l.debug(fmt.Sprint(v...)) + } +} + +func (l *richLogger) Debugf(format string, v ...any) { + if shallLog(DebugLevel) { + l.debug(fmt.Sprintf(format, v...)) + } +} + +func (l *richLogger) Debugv(v any) { + if shallLog(DebugLevel) { + l.debug(v) + } +} + +func (l *richLogger) Debugw(msg string, fields ...LogField) { + if shallLog(DebugLevel) { + l.debug(msg, fields...) + } +} + +func (l *richLogger) Error(v ...any) { + if shallLog(ErrorLevel) { + l.err(fmt.Sprint(v...)) + } +} + +func (l *richLogger) Errorf(format string, v ...any) { + if shallLog(ErrorLevel) { + l.err(fmt.Sprintf(format, v...)) + } +} + +func (l *richLogger) Errorv(v any) { + if shallLog(ErrorLevel) { + l.err(v) + } +} + +func (l *richLogger) Errorw(msg string, fields ...LogField) { + if shallLog(ErrorLevel) { + l.err(msg, fields...) + } +} + +func (l *richLogger) Info(v ...any) { + if shallLog(InfoLevel) { + l.info(fmt.Sprint(v...)) + } +} + +func (l *richLogger) Infof(format string, v ...any) { + if shallLog(InfoLevel) { + l.info(fmt.Sprintf(format, v...)) + } +} + +func (l *richLogger) Infov(v any) { + if shallLog(InfoLevel) { + l.info(v) + } +} + +func (l *richLogger) Infow(msg string, fields ...LogField) { + if shallLog(InfoLevel) { + l.info(msg, fields...) + } +} + +func (l *richLogger) Slow(v ...any) { + if shallLog(ErrorLevel) { + l.slow(fmt.Sprint(v...)) + } +} + +func (l *richLogger) Slowf(format string, v ...any) { + if shallLog(ErrorLevel) { + l.slow(fmt.Sprintf(format, v...)) + } +} + +func (l *richLogger) Slowv(v any) { + if shallLog(ErrorLevel) { + l.slow(v) + } +} + +func (l *richLogger) Sloww(msg string, fields ...LogField) { + if shallLog(ErrorLevel) { + l.slow(msg, fields...) + } +} + +func (l *richLogger) WithCallerSkip(skip int) Logger { + if skip <= 0 { + return l + } + + return &richLogger{ + ctx: l.ctx, + callerSkip: skip, + fields: l.fields, + } +} + +func (l *richLogger) WithContext(ctx context.Context) Logger { + return &richLogger{ + ctx: ctx, + callerSkip: l.callerSkip, + fields: l.fields, + } +} + +func (l *richLogger) WithDuration(duration time.Duration) Logger { + fields := append(l.fields, Field(durationKey, timex.ReprOfDuration(duration))) + + return &richLogger{ + ctx: l.ctx, + callerSkip: l.callerSkip, + fields: fields, + } +} + +func (l *richLogger) WithFields(fields ...LogField) Logger { + if len(fields) == 0 { + return l + } + + f := append(l.fields, fields...) + + return &richLogger{ + ctx: l.ctx, + callerSkip: l.callerSkip, + fields: f, + } +} + +func (l *richLogger) buildFields(fields ...LogField) []LogField { + fields = append(l.fields, fields...) + fields = append(fields, Field(callerKey, getCaller(callerDepth+l.callerSkip))) + + if l.ctx == nil { + return fields + } + + traceID := trace.TraceIDFromContext(l.ctx) + if len(traceID) > 0 { + fields = append(fields, Field(traceKey, traceID)) + } + + spanID := trace.SpanIDFromContext(l.ctx) + if len(spanID) > 0 { + fields = append(fields, Field(spanKey, spanID)) + } + + val := l.ctx.Value(fieldsContextKey) + if val != nil { + if arr, ok := val.([]LogField); ok { + fields = append(fields, arr...) + } + } + + return fields +} + +func (l *richLogger) debug(v any, fields ...LogField) { + if shallLog(DebugLevel) { + getWriter().Debug(v, l.buildFields(fields...)...) + } +} + +func (l *richLogger) err(v any, fields ...LogField) { + if shallLog(ErrorLevel) { + getWriter().Error(v, l.buildFields(fields...)...) + } +} + +func (l *richLogger) info(v any, fields ...LogField) { + if shallLog(InfoLevel) { + getWriter().Info(v, l.buildFields(fields...)...) + } +} + +func (l *richLogger) slow(v any, fields ...LogField) { + if shallLog(ErrorLevel) { + getWriter().Slow(v, l.buildFields(fields...)...) + } +} diff --git a/pkg/logger/richlogger_test.go b/pkg/logger/richlogger_test.go new file mode 100644 index 0000000..1eb2d2f --- /dev/null +++ b/pkg/logger/richlogger_test.go @@ -0,0 +1,408 @@ +package logger + +import ( + "context" + "encoding/json" + "fmt" + "io" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel" + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +func TestTraceLog(t *testing.T) { + SetLevel(InfoLevel) + w := new(mockWriter) + old := writer.Swap(w) + writer.lock.RLock() + defer func() { + writer.lock.RUnlock() + writer.Store(old) + }() + + otp := otel.GetTracerProvider() + tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample())) + otel.SetTracerProvider(tp) + defer otel.SetTracerProvider(otp) + + ctx, span := tp.Tracer("trace-id").Start(context.Background(), "span-id") + defer span.End() + + WithContext(ctx).Info(testlog) + validate(t, w.String(), true, true) +} + +func TestTraceDebug(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + writer.lock.RLock() + defer func() { + writer.lock.RUnlock() + writer.Store(old) + }() + + otp := otel.GetTracerProvider() + tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample())) + otel.SetTracerProvider(tp) + defer otel.SetTracerProvider(otp) + + ctx, span := tp.Tracer("foo").Start(context.Background(), "bar") + defer span.End() + + l := WithContext(ctx) + SetLevel(DebugLevel) + l.WithDuration(time.Second).Debug(testlog) + assert.True(t, strings.Contains(w.String(), traceKey)) + assert.True(t, strings.Contains(w.String(), spanKey)) + w.Reset() + l.WithDuration(time.Second).Debugf(testlog) + validate(t, w.String(), true, true) + w.Reset() + l.WithDuration(time.Second).Debugv(testlog) + validate(t, w.String(), true, true) + w.Reset() + l.WithDuration(time.Second).Debugv(testobj) + validateContentType(t, w.String(), map[string]any{}, true, true) + w.Reset() + l.WithDuration(time.Second).Debugw(testlog, Field("foo", "bar")) + validate(t, w.String(), true, true) + assert.True(t, strings.Contains(w.String(), "foo"), w.String()) + assert.True(t, strings.Contains(w.String(), "bar"), w.String()) +} + +func TestTraceError(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + writer.lock.RLock() + defer func() { + writer.lock.RUnlock() + writer.Store(old) + }() + + otp := otel.GetTracerProvider() + tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample())) + otel.SetTracerProvider(tp) + defer otel.SetTracerProvider(otp) + + ctx, span := tp.Tracer("trace-id").Start(context.Background(), "span-id") + defer span.End() + + var nilCtx context.Context + l := WithContext(context.Background()) + l = l.WithContext(nilCtx) + l = l.WithContext(ctx) + SetLevel(ErrorLevel) + l.WithDuration(time.Second).Error(testlog) + validate(t, w.String(), true, true) + w.Reset() + l.WithDuration(time.Second).Errorf(testlog) + validate(t, w.String(), true, true) + w.Reset() + l.WithDuration(time.Second).Errorv(testlog) + validate(t, w.String(), true, true) + w.Reset() + l.WithDuration(time.Second).Errorv(testobj) + validateContentType(t, w.String(), map[string]any{}, true, true) + w.Reset() + l.WithDuration(time.Second).Errorw(testlog, Field("basket", "ball")) + validate(t, w.String(), true, true) + assert.True(t, strings.Contains(w.String(), "basket"), w.String()) + assert.True(t, strings.Contains(w.String(), "ball"), w.String()) +} + +func TestTraceInfo(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + writer.lock.RLock() + defer func() { + writer.lock.RUnlock() + writer.Store(old) + }() + + otp := otel.GetTracerProvider() + tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample())) + otel.SetTracerProvider(tp) + defer otel.SetTracerProvider(otp) + + ctx, span := tp.Tracer("trace-id").Start(context.Background(), "span-id") + defer span.End() + + SetLevel(InfoLevel) + l := WithContext(ctx) + l.WithDuration(time.Second).Info(testlog) + validate(t, w.String(), true, true) + w.Reset() + l.WithDuration(time.Second).Infof(testlog) + validate(t, w.String(), true, true) + w.Reset() + l.WithDuration(time.Second).Infov(testlog) + validate(t, w.String(), true, true) + w.Reset() + l.WithDuration(time.Second).Infov(testobj) + validateContentType(t, w.String(), map[string]any{}, true, true) + w.Reset() + l.WithDuration(time.Second).Infow(testlog, Field("basket", "ball")) + validate(t, w.String(), true, true) + assert.True(t, strings.Contains(w.String(), "basket"), w.String()) + assert.True(t, strings.Contains(w.String(), "ball"), w.String()) +} + +func TestTraceInfoConsole(t *testing.T) { + old := atomic.SwapUint32(&encoding, jsonEncodingType) + defer atomic.StoreUint32(&encoding, old) + + w := new(mockWriter) + o := writer.Swap(w) + writer.lock.RLock() + defer func() { + writer.lock.RUnlock() + writer.Store(o) + }() + + otp := otel.GetTracerProvider() + tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample())) + otel.SetTracerProvider(tp) + defer otel.SetTracerProvider(otp) + + ctx, span := tp.Tracer("trace-id").Start(context.Background(), "span-id") + defer span.End() + + l := WithContext(ctx) + SetLevel(InfoLevel) + l.WithDuration(time.Second).Info(testlog) + validate(t, w.String(), true, true) + w.Reset() + l.WithDuration(time.Second).Infof(testlog) + validate(t, w.String(), true, true) + w.Reset() + l.WithDuration(time.Second).Infov(testlog) + validate(t, w.String(), true, true) + w.Reset() + l.WithDuration(time.Second).Infov(testobj) + validateContentType(t, w.String(), map[string]any{}, true, true) +} + +func TestTraceSlow(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + writer.lock.RLock() + defer func() { + writer.lock.RUnlock() + writer.Store(old) + }() + + otp := otel.GetTracerProvider() + tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample())) + otel.SetTracerProvider(tp) + defer otel.SetTracerProvider(otp) + + ctx, span := tp.Tracer("trace-id").Start(context.Background(), "span-id") + defer span.End() + + l := WithContext(ctx) + SetLevel(InfoLevel) + l.WithDuration(time.Second).Slow(testlog) + assert.True(t, strings.Contains(w.String(), traceKey)) + assert.True(t, strings.Contains(w.String(), spanKey)) + w.Reset() + l.WithDuration(time.Second).Slowf(testlog) + validate(t, w.String(), true, true) + w.Reset() + l.WithDuration(time.Second).Slowv(testlog) + validate(t, w.String(), true, true) + w.Reset() + l.WithDuration(time.Second).Slowv(testobj) + validateContentType(t, w.String(), map[string]any{}, true, true) + w.Reset() + l.WithDuration(time.Second).Sloww(testlog, Field("basket", "ball")) + validate(t, w.String(), true, true) + assert.True(t, strings.Contains(w.String(), "basket"), w.String()) + assert.True(t, strings.Contains(w.String(), "ball"), w.String()) +} + +func TestTraceWithoutContext(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + writer.lock.RLock() + defer func() { + writer.lock.RUnlock() + writer.Store(old) + }() + + l := WithContext(context.Background()) + SetLevel(InfoLevel) + l.WithDuration(time.Second).Info(testlog) + validate(t, w.String(), false, false) + w.Reset() + l.WithDuration(time.Second).Infof(testlog) + validate(t, w.String(), false, false) +} + +func TestLogWithFields(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + writer.lock.RLock() + defer func() { + writer.lock.RUnlock() + writer.Store(old) + }() + + ctx := ContextWithFields(context.Background(), Field("foo", "bar")) + l := WithContext(ctx) + SetLevel(InfoLevel) + l.Infow(testlog) + + var val mockValue + assert.Nil(t, json.Unmarshal([]byte(w.String()), &val)) + assert.Equal(t, "bar", val.Foo) +} + +func TestLogWithCallerSkip(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + writer.lock.RLock() + defer func() { + writer.lock.RUnlock() + writer.Store(old) + }() + + l := WithCallerSkip(1).WithCallerSkip(0) + p := func(v string) { + l.Infow(v) + } + + file, line := getFileLine() + p(testlog) + assert.True(t, w.Contains(fmt.Sprintf("%s:%d", file, line+1))) + + w.Reset() + l = WithCallerSkip(0).WithCallerSkip(1) + file, line = getFileLine() + p(testlog) + assert.True(t, w.Contains(fmt.Sprintf("%s:%d", file, line+1))) +} + +func TestLogWithCallerSkipCopy(t *testing.T) { + log1 := WithCallerSkip(2) + log2 := log1.WithCallerSkip(3) + log3 := log2.WithCallerSkip(-1) + assert.Equal(t, 2, log1.(*richLogger).callerSkip) + assert.Equal(t, 3, log2.(*richLogger).callerSkip) + assert.Equal(t, 3, log3.(*richLogger).callerSkip) +} + +func TestLogWithContextCopy(t *testing.T) { + c1 := context.Background() + type ctxKey string // 定义新的字符串类型 + + const fooKey ctxKey = "foo" // 使用这个 key + c2 := context.WithValue(context.Background(), fooKey, "bar") + log1 := WithContext(c1) + log2 := log1.WithContext(c2) + assert.Equal(t, c1, log1.(*richLogger).ctx) + assert.Equal(t, c2, log2.(*richLogger).ctx) +} + +func TestLogWithDurationCopy(t *testing.T) { + log1 := WithContext(context.Background()) + log2 := log1.WithDuration(time.Second) + assert.Empty(t, log1.(*richLogger).fields) + assert.Equal(t, 1, len(log2.(*richLogger).fields)) + + var w mockWriter + old := writer.Swap(&w) + defer writer.Store(old) + log2.Info("hello") + assert.Contains(t, w.String(), `"duration":"1000.0ms"`) +} + +func TestLogWithFieldsCopy(t *testing.T) { + log1 := WithContext(context.Background()) + log2 := log1.WithFields(Field("foo", "bar")) + log3 := log1.WithFields() + assert.Empty(t, log1.(*richLogger).fields) + assert.Equal(t, 1, len(log2.(*richLogger).fields)) + assert.Equal(t, log1, log3) + assert.Empty(t, log3.(*richLogger).fields) + + var w mockWriter + old := writer.Swap(&w) + defer writer.Store(old) + + log2.Info("hello") + assert.Contains(t, w.String(), `"foo":"bar"`) +} + +func TestLoggerWithFields(t *testing.T) { + w := new(mockWriter) + old := writer.Swap(w) + writer.lock.RLock() + defer func() { + writer.lock.RUnlock() + writer.Store(old) + }() + + l := WithContext(context.Background()).WithFields(Field("foo", "bar")) + l.Infow(testlog) + + var val mockValue + assert.Nil(t, json.Unmarshal([]byte(w.String()), &val)) + assert.Equal(t, "bar", val.Foo) +} + +func validate(t *testing.T, body string, expectedTrace, expectedSpan bool) { + var val mockValue + dec := json.NewDecoder(strings.NewReader(body)) + + for { + var doc mockValue + err := dec.Decode(&doc) + if err == io.EOF { + // all done + break + } + if err != nil { + continue + } + + val = doc + } + + assert.Equal(t, expectedTrace, len(val.Trace) > 0, body) + assert.Equal(t, expectedSpan, len(val.Span) > 0, body) +} + +func validateContentType(t *testing.T, body string, expectedType any, expectedTrace, expectedSpan bool) { + var val mockValue + dec := json.NewDecoder(strings.NewReader(body)) + + for { + var doc mockValue + err := dec.Decode(&doc) + if err == io.EOF { + // all done + break + } + if err != nil { + continue + } + + val = doc + } + + assert.IsType(t, expectedType, val.Content, body) + assert.Equal(t, expectedTrace, len(val.Trace) > 0, body) + assert.Equal(t, expectedSpan, len(val.Span) > 0, body) +} + +type mockValue struct { + Trace string `json:"trace"` + Span string `json:"span"` + Foo string `json:"foo"` + Content any `json:"content"` +} diff --git a/pkg/logger/rotatelogger.go b/pkg/logger/rotatelogger.go new file mode 100644 index 0000000..94411f6 --- /dev/null +++ b/pkg/logger/rotatelogger.go @@ -0,0 +1,468 @@ +package logger + +import ( + "compress/gzip" + "errors" + "fmt" + "log" + "os" + "path" + "path/filepath" + "sort" + "strings" + "sync" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/fs" + "github.com/perfect-panel/ppanel-server/pkg/lang" +) + +const ( + dateFormat = "2006-01-02" + hoursPerDay = 24 + bufferSize = 100 + defaultDirMode = 0o755 + defaultFileMode = 0o600 + gzipExt = ".gz" + megaBytes = 1 << 20 +) + +var ( + // ErrLogFileClosed is an error that indicates the log file is already closed. + ErrLogFileClosed = errors.New("error: log file closed") + + fileTimeFormat = time.RFC3339 +) + +type ( + // A RotateRule interface is used to define the log rotating rules. + RotateRule interface { + BackupFileName() string + MarkRotated() + OutdatedFiles() []string + ShallRotate(size int64) bool + } + + // A RotateLogger is a Logger that can rotate log files with given rules. + RotateLogger struct { + filename string + backup string + fp *os.File + channel chan []byte + done chan lang.PlaceholderType + rule RotateRule + compress bool + // can't use threading.RoutineGroup because of cycle import + waitGroup sync.WaitGroup + closeOnce sync.Once + currentSize int64 + } + + // A DailyRotateRule is a rule to daily rotate the log files. + DailyRotateRule struct { + rotatedTime string + filename string + delimiter string + days int + gzip bool + } + + // SizeLimitRotateRule a rotation rule that make the log file rotated base on size + SizeLimitRotateRule struct { + DailyRotateRule + maxSize int64 + maxBackups int + } +) + +// DefaultRotateRule is a default log rotating rule, currently DailyRotateRule. +func DefaultRotateRule(filename, delimiter string, days int, gzip bool) RotateRule { + return &DailyRotateRule{ + rotatedTime: getNowDate(), + filename: filename, + delimiter: delimiter, + days: days, + gzip: gzip, + } +} + +// BackupFileName returns the backup filename on rotating. +func (r *DailyRotateRule) BackupFileName() string { + return fmt.Sprintf("%s%s%s", r.filename, r.delimiter, getNowDate()) +} + +// MarkRotated marks the rotated time of r to be the current time. +func (r *DailyRotateRule) MarkRotated() { + r.rotatedTime = getNowDate() +} + +// OutdatedFiles returns the files that exceeded the keeping days. +func (r *DailyRotateRule) OutdatedFiles() []string { + if r.days <= 0 { + return nil + } + + var pattern string + if r.gzip { + pattern = fmt.Sprintf("%s%s*%s", r.filename, r.delimiter, gzipExt) + } else { + pattern = fmt.Sprintf("%s%s*", r.filename, r.delimiter) + } + + files, err := filepath.Glob(pattern) + if err != nil { + Errorf("failed to delete outdated log files, error: %s", err) + return nil + } + + var buf strings.Builder + boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay*r.days)).Format(dateFormat) + buf.WriteString(r.filename) + buf.WriteString(r.delimiter) + buf.WriteString(boundary) + if r.gzip { + buf.WriteString(gzipExt) + } + boundaryFile := buf.String() + + var outdates []string + for _, file := range files { + if file < boundaryFile { + outdates = append(outdates, file) + } + } + + return outdates +} + +// ShallRotate checks if the file should be rotated. +func (r *DailyRotateRule) ShallRotate(_ int64) bool { + return len(r.rotatedTime) > 0 && getNowDate() != r.rotatedTime +} + +// NewSizeLimitRotateRule returns the rotation rule with size limit +func NewSizeLimitRotateRule(filename, delimiter string, days, maxSize, maxBackups int, gzip bool) RotateRule { + return &SizeLimitRotateRule{ + DailyRotateRule: DailyRotateRule{ + rotatedTime: getNowDateInRFC3339Format(), + filename: filename, + delimiter: delimiter, + days: days, + gzip: gzip, + }, + maxSize: int64(maxSize) * megaBytes, + maxBackups: maxBackups, + } +} + +func (r *SizeLimitRotateRule) BackupFileName() string { + dir := filepath.Dir(r.filename) + prefix, ext := r.parseFilename() + timestamp := getNowDateInRFC3339Format() + return filepath.Join(dir, fmt.Sprintf("%s%s%s%s", prefix, r.delimiter, timestamp, ext)) +} + +func (r *SizeLimitRotateRule) MarkRotated() { + r.rotatedTime = getNowDateInRFC3339Format() +} + +func (r *SizeLimitRotateRule) OutdatedFiles() []string { + dir := filepath.Dir(r.filename) + prefix, ext := r.parseFilename() + + var pattern string + if r.gzip { + pattern = fmt.Sprintf("%s%s%s%s*%s%s", dir, string(filepath.Separator), + prefix, r.delimiter, ext, gzipExt) + } else { + pattern = fmt.Sprintf("%s%s%s%s*%s", dir, string(filepath.Separator), + prefix, r.delimiter, ext) + } + + files, err := filepath.Glob(pattern) + if err != nil { + Errorf("failed to delete outdated log files, error: %s", err) + return nil + } + + sort.Strings(files) + + outdated := make(map[string]lang.PlaceholderType) + + // test if too many backups + if r.maxBackups > 0 && len(files) > r.maxBackups { + for _, f := range files[:len(files)-r.maxBackups] { + outdated[f] = lang.Placeholder + } + files = files[len(files)-r.maxBackups:] + } + + // test if any too old backups + if r.days > 0 { + boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay*r.days)).Format(fileTimeFormat) + boundaryFile := filepath.Join(dir, fmt.Sprintf("%s%s%s%s", prefix, r.delimiter, boundary, ext)) + if r.gzip { + boundaryFile += gzipExt + } + for _, f := range files { + if f >= boundaryFile { + break + } + outdated[f] = lang.Placeholder + } + } + + var result []string + for k := range outdated { + result = append(result, k) + } + return result +} + +func (r *SizeLimitRotateRule) ShallRotate(size int64) bool { + return r.maxSize > 0 && r.maxSize < size +} + +func (r *SizeLimitRotateRule) parseFilename() (prefix, ext string) { + logName := filepath.Base(r.filename) + ext = filepath.Ext(r.filename) + prefix = logName[:len(logName)-len(ext)] + return +} + +// NewLogger returns a RotateLogger with given filename and rule, etc. +func NewLogger(filename string, rule RotateRule, compress bool) (*RotateLogger, error) { + l := &RotateLogger{ + filename: filename, + channel: make(chan []byte, bufferSize), + done: make(chan lang.PlaceholderType), + rule: rule, + compress: compress, + } + if err := l.initialize(); err != nil { + return nil, err + } + + l.startWorker() + return l, nil +} + +// Close closes l. +func (l *RotateLogger) Close() error { + var err error + + l.closeOnce.Do(func() { + close(l.done) + l.waitGroup.Wait() + + if err = l.fp.Sync(); err != nil { + return + } + + err = l.fp.Close() + }) + + return err +} + +func (l *RotateLogger) Write(data []byte) (int, error) { + select { + case l.channel <- data: + return len(data), nil + case <-l.done: + log.Println(string(data)) + return 0, ErrLogFileClosed + } +} + +func (l *RotateLogger) getBackupFilename() string { + if len(l.backup) == 0 { + return l.rule.BackupFileName() + } + + return l.backup +} + +func (l *RotateLogger) initialize() error { + l.backup = l.rule.BackupFileName() + + if fileInfo, err := os.Stat(l.filename); err != nil { + basePath := path.Dir(l.filename) + if _, err = os.Stat(basePath); err != nil { + if err = os.MkdirAll(basePath, defaultDirMode); err != nil { + return err + } + } + + if l.fp, err = os.Create(l.filename); err != nil { + return err + } + } else { + if l.fp, err = os.OpenFile(l.filename, os.O_APPEND|os.O_WRONLY, defaultFileMode); err != nil { + return err + } + + l.currentSize = fileInfo.Size() + } + + fs.CloseOnExec(l.fp) + + return nil +} + +func (l *RotateLogger) maybeCompressFile(file string) { + if !l.compress { + return + } + + defer func() { + if r := recover(); r != nil { + ErrorStack(r) + } + }() + + if _, err := os.Stat(file); err != nil { + // file doesn't exist or another error, ignore compression + return + } + + compressLogFile(file) +} + +func (l *RotateLogger) maybeDeleteOutdatedFiles() { + files := l.rule.OutdatedFiles() + for _, file := range files { + if err := os.Remove(file); err != nil { + Errorf("failed to remove outdated file: %s", file) + } + } +} + +func (l *RotateLogger) postRotate(file string) { + go func() { + // we cannot use threading.GoSafe here, because of import cycle. + l.maybeCompressFile(file) + l.maybeDeleteOutdatedFiles() + }() +} + +func (l *RotateLogger) rotate() error { + if l.fp != nil { + err := l.fp.Close() + l.fp = nil + if err != nil { + return err + } + } + + _, err := os.Stat(l.filename) + if err == nil && len(l.backup) > 0 { + backupFilename := l.getBackupFilename() + err = os.Rename(l.filename, backupFilename) + if err != nil { + return err + } + + l.postRotate(backupFilename) + } + + l.backup = l.rule.BackupFileName() + if l.fp, err = os.Create(l.filename); err == nil { + fs.CloseOnExec(l.fp) + } + + return err +} + +func (l *RotateLogger) startWorker() { + l.waitGroup.Add(1) + + go func() { + defer l.waitGroup.Done() + + for { + select { + case event := <-l.channel: + l.write(event) + case <-l.done: + // avoid losing logs before closing. + for { + select { + case event := <-l.channel: + l.write(event) + default: + return + } + } + } + } + }() +} + +func (l *RotateLogger) write(v []byte) { + if l.rule.ShallRotate(l.currentSize + int64(len(v))) { + if err := l.rotate(); err != nil { + log.Println(err) + } else { + l.rule.MarkRotated() + l.currentSize = 0 + } + } + if l.fp != nil { + _, _ = l.fp.Write(v) + l.currentSize += int64(len(v)) + } +} + +func compressLogFile(file string) { + start := time.Now() + Infof("compressing log file: %s", file) + if err := gzipFile(file, fileSys); err != nil { + Errorf("compress error: %s", err) + } else { + Infof("compressed log file: %s, took %s", file, time.Since(start)) + } +} + +func getNowDate() string { + return time.Now().Format(dateFormat) +} + +func getNowDateInRFC3339Format() string { + return time.Now().Format(fileTimeFormat) +} + +func gzipFile(file string, fsys fileSystem) (err error) { + in, err := fsys.Open(file) + if err != nil { + return err + } + defer func() { + if e := fsys.Close(in); e != nil { + Errorf("failed to close file: %s, error: %v", file, e) + } + if err == nil { + // only remove the original file when compression is successful + err = fsys.Remove(file) + } + }() + + out, err := fsys.Create(fmt.Sprintf("%s%s", file, gzipExt)) + if err != nil { + return err + } + defer func() { + e := fsys.Close(out) + if err == nil { + err = e + } + }() + + w := gzip.NewWriter(out) + if _, err = fsys.Copy(w, in); err != nil { + // failed to copy, no need to close w + return err + } + + return fsys.Close(w) +} diff --git a/pkg/logger/rotatelogger_test.go b/pkg/logger/rotatelogger_test.go new file mode 100644 index 0000000..763dce4 --- /dev/null +++ b/pkg/logger/rotatelogger_test.go @@ -0,0 +1,636 @@ +package logger + +import ( + "errors" + "io" + "os" + "path" + "path/filepath" + "sync/atomic" + "syscall" + "testing" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/random" + + "github.com/perfect-panel/ppanel-server/pkg/fs" + "github.com/stretchr/testify/assert" +) + +func TestDailyRotateRuleMarkRotated(t *testing.T) { + t.Run("daily rule", func(t *testing.T) { + var rule DailyRotateRule + rule.MarkRotated() + assert.Equal(t, getNowDate(), rule.rotatedTime) + }) + + t.Run("daily rule", func(t *testing.T) { + rule := DefaultRotateRule("test", "-", 1, false) + _, ok := rule.(*DailyRotateRule) + assert.True(t, ok) + }) +} + +func TestDailyRotateRuleOutdatedFiles(t *testing.T) { + t.Run("no files", func(t *testing.T) { + var rule DailyRotateRule + assert.Empty(t, rule.OutdatedFiles()) + rule.days = 1 + assert.Empty(t, rule.OutdatedFiles()) + rule.gzip = true + assert.Empty(t, rule.OutdatedFiles()) + }) + + t.Run("bad files", func(t *testing.T) { + rule := DailyRotateRule{ + filename: "[a-z", + } + assert.Empty(t, rule.OutdatedFiles()) + rule.days = 1 + assert.Empty(t, rule.OutdatedFiles()) + rule.gzip = true + assert.Empty(t, rule.OutdatedFiles()) + }) + + t.Run("temp files", func(t *testing.T) { + boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay) * 2).Format(dateFormat) + f1, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary) + assert.NoError(t, err) + _ = f1.Close() + f2, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary) + assert.NoError(t, err) + _ = f2.Close() + t.Cleanup(func() { + _ = os.Remove(f1.Name()) + _ = os.Remove(f2.Name()) + }) + rule := DailyRotateRule{ + filename: path.Join(os.TempDir(), "go-zero-test-"), + days: 1, + } + assert.NotEmpty(t, rule.OutdatedFiles()) + }) +} + +func TestDailyRotateRuleShallRotate(t *testing.T) { + var rule DailyRotateRule + rule.rotatedTime = time.Now().Add(time.Hour * 24).Format(dateFormat) + assert.True(t, rule.ShallRotate(0)) +} + +func TestSizeLimitRotateRuleMarkRotated(t *testing.T) { + t.Run("size limit rule", func(t *testing.T) { + var rule SizeLimitRotateRule + rule.MarkRotated() + assert.Equal(t, getNowDateInRFC3339Format(), rule.rotatedTime) + }) + + t.Run("size limit rule", func(t *testing.T) { + rule := NewSizeLimitRotateRule("foo", "-", 1, 1, 1, false) + rule.MarkRotated() + assert.Equal(t, getNowDateInRFC3339Format(), rule.(*SizeLimitRotateRule).rotatedTime) + }) +} + +func TestSizeLimitRotateRuleOutdatedFiles(t *testing.T) { + t.Run("no files", func(t *testing.T) { + var rule SizeLimitRotateRule + assert.Empty(t, rule.OutdatedFiles()) + rule.days = 1 + assert.Empty(t, rule.OutdatedFiles()) + rule.gzip = true + assert.Empty(t, rule.OutdatedFiles()) + rule.maxBackups = 0 + assert.Empty(t, rule.OutdatedFiles()) + }) + + t.Run("bad files", func(t *testing.T) { + rule := SizeLimitRotateRule{ + DailyRotateRule: DailyRotateRule{ + filename: "[a-z", + }, + } + assert.Empty(t, rule.OutdatedFiles()) + rule.days = 1 + assert.Empty(t, rule.OutdatedFiles()) + rule.gzip = true + assert.Empty(t, rule.OutdatedFiles()) + }) + + t.Run("temp files", func(t *testing.T) { + boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay) * 2).Format(dateFormat) + f1, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary) + assert.NoError(t, err) + f2, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary) + assert.NoError(t, err) + boundary1 := time.Now().Add(time.Hour * time.Duration(hoursPerDay) * 2).Format(dateFormat) + f3, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary1) + assert.NoError(t, err) + t.Cleanup(func() { + _ = f1.Close() + _ = os.Remove(f1.Name()) + _ = f2.Close() + _ = os.Remove(f2.Name()) + _ = f3.Close() + _ = os.Remove(f3.Name()) + }) + rule := SizeLimitRotateRule{ + DailyRotateRule: DailyRotateRule{ + filename: path.Join(os.TempDir(), "go-zero-test-"), + days: 1, + }, + maxBackups: 3, + } + assert.NotEmpty(t, rule.OutdatedFiles()) + }) + + t.Run("no backups", func(t *testing.T) { + boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay) * 2).Format(dateFormat) + f1, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary) + assert.NoError(t, err) + f2, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary) + assert.NoError(t, err) + boundary1 := time.Now().Add(time.Hour * time.Duration(hoursPerDay) * 2).Format(dateFormat) + f3, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary1) + assert.NoError(t, err) + t.Cleanup(func() { + _ = f1.Close() + _ = os.Remove(f1.Name()) + _ = f2.Close() + _ = os.Remove(f2.Name()) + _ = f3.Close() + _ = os.Remove(f3.Name()) + }) + rule := SizeLimitRotateRule{ + DailyRotateRule: DailyRotateRule{ + filename: path.Join(os.TempDir(), "go-zero-test-"), + days: 1, + }, + } + assert.NotEmpty(t, rule.OutdatedFiles()) + + logger := new(RotateLogger) + logger.rule = &rule + logger.maybeDeleteOutdatedFiles() + assert.Empty(t, rule.OutdatedFiles()) + }) +} + +func TestSizeLimitRotateRuleShallRotate(t *testing.T) { + var rule SizeLimitRotateRule + rule.rotatedTime = time.Now().Add(time.Hour * 24).Format(fileTimeFormat) + rule.maxSize = 0 + assert.False(t, rule.ShallRotate(0)) + rule.maxSize = 100 + assert.False(t, rule.ShallRotate(0)) + assert.True(t, rule.ShallRotate(101*megaBytes)) +} + +func TestRotateLoggerClose(t *testing.T) { + t.Run("close", func(t *testing.T) { + filename, err := fs.TempFilenameWithText("foo") + assert.Nil(t, err) + if len(filename) > 0 { + defer os.Remove(filename) + } + logger, err := NewLogger(filename, new(DailyRotateRule), false) + assert.Nil(t, err) + _, err = logger.Write([]byte("foo")) + assert.Nil(t, err) + assert.Nil(t, logger.Close()) + }) + + t.Run("close and write", func(t *testing.T) { + logger := new(RotateLogger) + logger.done = make(chan struct{}) + close(logger.done) + _, err := logger.Write([]byte("foo")) + assert.ErrorIs(t, err, ErrLogFileClosed) + }) + + t.Run("close without losing logs", func(t *testing.T) { + text := "foo" + filename, err := fs.TempFilenameWithText(text) + assert.Nil(t, err) + if len(filename) > 0 { + defer os.Remove(filename) + } + logger, err := NewLogger(filename, new(DailyRotateRule), false) + assert.Nil(t, err) + msg := []byte("foo") + n := 100 + for i := 0; i < n; i++ { + _, err = logger.Write(msg) + assert.Nil(t, err) + } + assert.Nil(t, logger.Close()) + bs, err := os.ReadFile(filename) + assert.Nil(t, err) + assert.Equal(t, len(msg)*n+len(text), len(bs)) + }) +} + +func TestRotateLoggerGetBackupFilename(t *testing.T) { + filename, err := fs.TempFilenameWithText("foo") + assert.Nil(t, err) + if len(filename) > 0 { + defer os.Remove(filename) + } + logger, err := NewLogger(filename, new(DailyRotateRule), false) + assert.Nil(t, err) + assert.True(t, len(logger.getBackupFilename()) > 0) + logger.backup = "" + assert.True(t, len(logger.getBackupFilename()) > 0) +} + +func TestRotateLoggerMayCompressFile(t *testing.T) { + old := os.Stdout + os.Stdout = os.NewFile(0, os.DevNull) + defer func() { + os.Stdout = old + }() + + filename, err := fs.TempFilenameWithText("foo") + assert.Nil(t, err) + if len(filename) > 0 { + defer os.Remove(filename) + } + logger, err := NewLogger(filename, new(DailyRotateRule), false) + assert.Nil(t, err) + logger.maybeCompressFile(filename) + _, err = os.Stat(filename) + assert.Nil(t, err) +} + +func TestRotateLoggerMayCompressFileTrue(t *testing.T) { + old := os.Stdout + os.Stdout = os.NewFile(0, os.DevNull) + defer func() { + os.Stdout = old + }() + + filename, err := fs.TempFilenameWithText("foo") + assert.Nil(t, err) + logger, err := NewLogger(filename, new(DailyRotateRule), true) + assert.Nil(t, err) + if len(filename) > 0 { + defer os.Remove(filepath.Base(logger.getBackupFilename()) + ".gz") + } + logger.maybeCompressFile(filename) + _, err = os.Stat(filename) + assert.NotNil(t, err) +} + +func TestRotateLoggerRotate(t *testing.T) { + filename, err := fs.TempFilenameWithText("foo") + assert.Nil(t, err) + logger, err := NewLogger(filename, new(DailyRotateRule), true) + assert.Nil(t, err) + if len(filename) > 0 { + defer func() { + os.Remove(logger.getBackupFilename()) + os.Remove(filepath.Base(logger.getBackupFilename()) + ".gz") + }() + } + err = logger.rotate() + switch v := err.(type) { + case *os.LinkError: + // avoid rename error on docker container + assert.Equal(t, syscall.EXDEV, v.Err) + case *os.PathError: + // ignore remove error for tests, + // files are cleaned in GitHub actions. + assert.Equal(t, "remove", v.Op) + default: + assert.Nil(t, err) + } +} + +func TestRotateLoggerWrite(t *testing.T) { + filename, err := fs.TempFilenameWithText("foo") + assert.Nil(t, err) + rule := new(DailyRotateRule) + logger, err := NewLogger(filename, rule, true) + assert.Nil(t, err) + if len(filename) > 0 { + defer func() { + os.Remove(logger.getBackupFilename()) + os.Remove(filepath.Base(logger.getBackupFilename()) + ".gz") + }() + } + // the following write calls cannot be changed to Write, because of DATA RACE. + logger.write([]byte(`foo`)) + rule.rotatedTime = time.Now().Add(-time.Hour * 24).Format(dateFormat) + logger.write([]byte(`bar`)) + logger.Close() + logger.write([]byte(`baz`)) +} + +func TestLogWriterClose(t *testing.T) { + assert.Nil(t, newLogWriter(nil).Close()) +} + +func TestRotateLoggerWithSizeLimitRotateRuleClose(t *testing.T) { + filename, err := fs.TempFilenameWithText("foo") + assert.Nil(t, err) + if len(filename) > 0 { + defer os.Remove(filename) + } + logger, err := NewLogger(filename, new(SizeLimitRotateRule), false) + assert.Nil(t, err) + _ = logger.Close() +} + +func TestRotateLoggerGetBackupWithSizeLimitRotateRuleFilename(t *testing.T) { + filename, err := fs.TempFilenameWithText("foo") + assert.Nil(t, err) + if len(filename) > 0 { + defer os.Remove(filename) + } + logger, err := NewLogger(filename, new(SizeLimitRotateRule), false) + assert.Nil(t, err) + assert.True(t, len(logger.getBackupFilename()) > 0) + logger.backup = "" + assert.True(t, len(logger.getBackupFilename()) > 0) +} + +func TestRotateLoggerWithSizeLimitRotateRuleMayCompressFile(t *testing.T) { + old := os.Stdout + os.Stdout = os.NewFile(0, os.DevNull) + defer func() { + os.Stdout = old + }() + + filename, err := fs.TempFilenameWithText("foo") + assert.Nil(t, err) + if len(filename) > 0 { + defer os.Remove(filename) + } + logger, err := NewLogger(filename, new(SizeLimitRotateRule), false) + assert.Nil(t, err) + logger.maybeCompressFile(filename) + _, err = os.Stat(filename) + assert.Nil(t, err) +} + +func TestRotateLoggerWithSizeLimitRotateRuleMayCompressFileTrue(t *testing.T) { + old := os.Stdout + os.Stdout = os.NewFile(0, os.DevNull) + defer func() { + os.Stdout = old + }() + + filename, err := fs.TempFilenameWithText("foo") + assert.Nil(t, err) + logger, err := NewLogger(filename, new(SizeLimitRotateRule), true) + assert.Nil(t, err) + if len(filename) > 0 { + defer os.Remove(filepath.Base(logger.getBackupFilename()) + ".gz") + } + logger.maybeCompressFile(filename) + _, err = os.Stat(filename) + assert.NotNil(t, err) +} + +func TestRotateLoggerWithSizeLimitRotateRuleMayCompressFileFailed(t *testing.T) { + old := os.Stdout + os.Stdout = os.NewFile(0, os.DevNull) + defer func() { + os.Stdout = old + }() + + filename := random.KeyNew(8, 1) + logger, err := NewLogger(filename, new(SizeLimitRotateRule), true) + defer os.Remove(filename) + if assert.NoError(t, err) { + assert.NotPanics(t, func() { + logger.maybeCompressFile(random.KeyNew(8, 1)) + }) + } +} + +func TestRotateLoggerWithSizeLimitRotateRuleRotate(t *testing.T) { + filename, err := fs.TempFilenameWithText("foo") + assert.Nil(t, err) + logger, err := NewLogger(filename, new(SizeLimitRotateRule), true) + assert.Nil(t, err) + if len(filename) > 0 { + defer func() { + os.Remove(logger.getBackupFilename()) + os.Remove(filepath.Base(logger.getBackupFilename()) + ".gz") + }() + } + err = logger.rotate() + switch v := err.(type) { + case *os.LinkError: + // avoid rename error on docker container + assert.Equal(t, syscall.EXDEV, v.Err) + case *os.PathError: + // ignore remove error for tests, + // files are cleaned in GitHub actions. + assert.Equal(t, "remove", v.Op) + default: + assert.Nil(t, err) + } +} + +func TestRotateLoggerWithSizeLimitRotateRuleWrite(t *testing.T) { + filename, err := fs.TempFilenameWithText("foo") + assert.Nil(t, err) + rule := new(SizeLimitRotateRule) + logger, err := NewLogger(filename, rule, true) + assert.Nil(t, err) + if len(filename) > 0 { + defer func() { + os.Remove(logger.getBackupFilename()) + os.Remove(filepath.Base(logger.getBackupFilename()) + ".gz") + }() + } + // the following write calls cannot be changed to Write, because of DATA RACE. + logger.write([]byte(`foo`)) + rule.rotatedTime = time.Now().Add(-time.Hour * 24).Format(dateFormat) + logger.write([]byte(`bar`)) + logger.Close() + logger.write([]byte(`baz`)) +} + +func TestGzipFile(t *testing.T) { + err := errors.New("any error") + + t.Run("gzip file open failed", func(t *testing.T) { + fsys := &fakeFileSystem{ + openFn: func(name string) (*os.File, error) { + return nil, err + }, + } + assert.ErrorIs(t, err, gzipFile("any", fsys)) + assert.False(t, fsys.Removed()) + }) + + t.Run("gzip file create failed", func(t *testing.T) { + fsys := &fakeFileSystem{ + createFn: func(name string) (*os.File, error) { + return nil, err + }, + } + assert.ErrorIs(t, err, gzipFile("any", fsys)) + assert.False(t, fsys.Removed()) + }) + + t.Run("gzip file copy failed", func(t *testing.T) { + fsys := &fakeFileSystem{ + copyFn: func(writer io.Writer, reader io.Reader) (int64, error) { + return 0, err + }, + } + assert.ErrorIs(t, err, gzipFile("any", fsys)) + assert.False(t, fsys.Removed()) + }) + + t.Run("gzip file last close failed", func(t *testing.T) { + var called int32 + fsys := &fakeFileSystem{ + closeFn: func(closer io.Closer) error { + if atomic.AddInt32(&called, 1) > 2 { + return err + } + return nil + }, + } + assert.NoError(t, gzipFile("any", fsys)) + assert.True(t, fsys.Removed()) + }) + + t.Run("gzip file remove failed", func(t *testing.T) { + fsys := &fakeFileSystem{ + removeFn: func(name string) error { + return err + }, + } + assert.Error(t, err, gzipFile("any", fsys)) + assert.True(t, fsys.Removed()) + }) + + t.Run("gzip file everything ok", func(t *testing.T) { + fsys := &fakeFileSystem{} + assert.NoError(t, gzipFile("any", fsys)) + assert.True(t, fsys.Removed()) + }) +} + +func TestRotateLogger_WithExistingFile(t *testing.T) { + const body = "foo" + filename, err := fs.TempFilenameWithText(body) + assert.Nil(t, err) + if len(filename) > 0 { + defer os.Remove(filename) + } + + rule := NewSizeLimitRotateRule(filename, "-", 1, 100, 3, false) + logger, err := NewLogger(filename, rule, false) + assert.Nil(t, err) + assert.Equal(t, int64(len(body)), logger.currentSize) + assert.Nil(t, logger.Close()) +} + +func BenchmarkRotateLogger(b *testing.B) { + filename := "./test.log" + filename2 := "./test2.log" + dailyRotateRuleLogger, err1 := NewLogger( + filename, + DefaultRotateRule( + filename, + backupFileDelimiter, + 1, + true, + ), + true, + ) + if err1 != nil { + b.Logf("Failed to new daily rotate rule logger: %v", err1) + b.FailNow() + } + sizeLimitRotateRuleLogger, err2 := NewLogger( + filename2, + NewSizeLimitRotateRule( + filename, + backupFileDelimiter, + 1, + 100, + 10, + true, + ), + true, + ) + if err2 != nil { + b.Logf("Failed to new size limit rotate rule logger: %v", err1) + b.FailNow() + } + defer func() { + dailyRotateRuleLogger.Close() + sizeLimitRotateRuleLogger.Close() + os.Remove(filename) + os.Remove(filename2) + }() + + b.Run("daily rotate rule", func(b *testing.B) { + for i := 0; i < b.N; i++ { + dailyRotateRuleLogger.write([]byte("testing\ntesting\n")) + } + }) + b.Run("size limit rotate rule", func(b *testing.B) { + for i := 0; i < b.N; i++ { + sizeLimitRotateRuleLogger.write([]byte("testing\ntesting\n")) + } + }) +} + +type fakeFileSystem struct { + removed int32 + closeFn func(closer io.Closer) error + copyFn func(writer io.Writer, reader io.Reader) (int64, error) + createFn func(name string) (*os.File, error) + openFn func(name string) (*os.File, error) + removeFn func(name string) error +} + +func (f *fakeFileSystem) Close(closer io.Closer) error { + if f.closeFn != nil { + return f.closeFn(closer) + } + return nil +} + +func (f *fakeFileSystem) Copy(writer io.Writer, reader io.Reader) (int64, error) { + if f.copyFn != nil { + return f.copyFn(writer, reader) + } + return 0, nil +} + +func (f *fakeFileSystem) Create(name string) (*os.File, error) { + if f.createFn != nil { + return f.createFn(name) + } + return nil, nil +} + +func (f *fakeFileSystem) Open(name string) (*os.File, error) { + if f.openFn != nil { + return f.openFn(name) + } + return nil, nil +} + +func (f *fakeFileSystem) Remove(name string) error { + atomic.AddInt32(&f.removed, 1) + + if f.removeFn != nil { + return f.removeFn(name) + } + return nil +} + +func (f *fakeFileSystem) Removed() bool { + return atomic.LoadInt32(&f.removed) > 0 +} diff --git a/pkg/logger/syslog.go b/pkg/logger/syslog.go new file mode 100644 index 0000000..2cf2e9c --- /dev/null +++ b/pkg/logger/syslog.go @@ -0,0 +1,15 @@ +package logger + +import "log" + +type redirector struct{} + +// CollectSysLog redirects system log into logx info +func CollectSysLog() { + log.SetOutput(new(redirector)) +} + +func (r *redirector) Write(p []byte) (n int, err error) { + Info(string(p)) + return len(p), nil +} diff --git a/pkg/logger/syslog_test.go b/pkg/logger/syslog_test.go new file mode 100644 index 0000000..8e98aa8 --- /dev/null +++ b/pkg/logger/syslog_test.go @@ -0,0 +1,61 @@ +package logger + +import ( + "encoding/json" + "log" + "strings" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" +) + +const testlog = "Stay hungry, stay foolish." + +var testobj = map[string]any{"foo": "bar"} + +func TestCollectSysLog(t *testing.T) { + CollectSysLog() + content := getContent(captureOutput(func() { + log.Print(testlog) + })) + assert.True(t, strings.Contains(content, testlog)) +} + +func TestRedirector(t *testing.T) { + var r redirector + content := getContent(captureOutput(func() { + _, _ = r.Write([]byte(testlog)) + })) + assert.Equal(t, testlog, content) +} + +func captureOutput(f func()) string { + w := new(mockWriter) + old := writer.Swap(w) + defer writer.Store(old) + + prevLevel := atomic.LoadUint32(&logLevel) + SetLevel(InfoLevel) + f() + SetLevel(prevLevel) + + return w.String() +} + +func getContent(jsonStr string) string { + var entry map[string]any + _ = json.Unmarshal([]byte(jsonStr), &entry) + + val, ok := entry[contentKey] + if !ok { + return "" + } + + str, ok := val.(string) + if !ok { + return "" + } + + return str +} diff --git a/pkg/logger/util.go b/pkg/logger/util.go new file mode 100644 index 0000000..001b803 --- /dev/null +++ b/pkg/logger/util.go @@ -0,0 +1,61 @@ +package logger + +import ( + "fmt" + "runtime" + "strings" + "time" +) + +func getCaller(callDepth int) string { + var file string + var line int + var ok bool + noMatch := []string{"logger", "@", "model", "default"} + + if callDepth > 0 { + _, file, line, ok = runtime.Caller(callDepth) + if !ok { + return "" + } + } else { + // skip logger and External library + for i := 0; i < 20; i++ { + _, file, line, ok = runtime.Caller(i) + if !ok { + return "" + } + if !contains(noMatch, file) { + break + } + } + } + return prettyCaller(file, line) +} + +func getTimestamp() string { + return time.Now().Format(timeFormat) +} + +func prettyCaller(file string, line int) string { + idx := strings.LastIndexByte(file, '/') + if idx < 0 { + return fmt.Sprintf("%s:%d", file, line) + } + + idx = strings.LastIndexByte(file[:idx], '/') + if idx < 0 { + return fmt.Sprintf("%s:%d", file, line) + } + + return fmt.Sprintf("%s:%d", file[idx+1:], line) +} + +func contains(slice []string, item string) bool { + for _, s := range slice { + if strings.Contains(item, s) { + return true + } + } + return false +} diff --git a/pkg/logger/util_test.go b/pkg/logger/util_test.go new file mode 100644 index 0000000..a516c49 --- /dev/null +++ b/pkg/logger/util_test.go @@ -0,0 +1,72 @@ +package logger + +import ( + "path/filepath" + "runtime" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestGetCaller(t *testing.T) { + _, file, _, _ := runtime.Caller(0) + assert.Contains(t, getCaller(1), filepath.Base(file)) + assert.True(t, len(getCaller(1<<10)) == 0) +} + +func TestGetTimestamp(t *testing.T) { + ts := getTimestamp() + tm, err := time.Parse(timeFormat, ts) + assert.Nil(t, err) + assert.True(t, time.Since(tm) < time.Minute) +} + +func TestPrettyCaller(t *testing.T) { + tests := []struct { + name string + file string + line int + want string + }{ + { + name: "regular", + file: "logx_test.go", + line: 123, + want: "logx_test.go:123", + }, + { + name: "relative", + file: "adhoc/logx_test.go", + line: 123, + want: "adhoc/logx_test.go:123", + }, + { + name: "long path", + file: "github.com/zeromicro/go-zero/core/logx/util_test.go", + line: 12, + want: "logx/util_test.go:12", + }, + { + name: "local path", + file: "/Users/kevin/go-zero/core/logx/util_test.go", + line: 1234, + want: "logx/util_test.go:1234", + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.want, prettyCaller(test.file, test.line)) + }) + } +} + +func BenchmarkGetCaller(b *testing.B) { + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + getCaller(1) + } +} diff --git a/pkg/logger/vars.go b/pkg/logger/vars.go new file mode 100644 index 0000000..f43c526 --- /dev/null +++ b/pkg/logger/vars.go @@ -0,0 +1,75 @@ +package logger + +import ( + "errors" + + "github.com/perfect-panel/ppanel-server/pkg/syncx" +) + +const ( + // DebugLevel logs everything + DebugLevel uint32 = iota + // InfoLevel does not include debugs + InfoLevel + // ErrorLevel includes errors, slows, stacks + ErrorLevel + // SevereLevel only log severe messages + SevereLevel + // disableLevel doesn't log any messages + disableLevel = 0xff +) + +const ( + jsonEncodingType = iota + plainEncodingType +) + +const ( + plainEncoding = "plain" + plainEncodingSep = '\t' + sizeRotationRule = "size" + + accessFilename = "access.log" + errorFilename = "error.log" + severeFilename = "severe.log" + slowFilename = "slow.log" + statFilename = "stat.log" + + fileMode = "file" + volumeMode = "volume" + + levelAlert = "alert" + levelInfo = "info" + levelError = "error" + levelSevere = "severe" + levelFatal = "fatal" + levelSlow = "slow" + levelStat = "stat" + levelDebug = "debug" + + backupFileDelimiter = "-" + nilAngleString = "" + flags = 0x0 +) + +const ( + callerKey = "caller" + contentKey = "content" + durationKey = "duration" + levelKey = "level" + spanKey = "span" + timestampKey = "timestamp" + traceKey = "trace" + truncatedKey = "truncated" +) + +var ( + // ErrLogPathNotSet is an error that indicates the log path is not set. + ErrLogPathNotSet = errors.New("log path must be set") + // ErrLogServiceNameNotSet is an error that indicates that the service name is not set. + ErrLogServiceNameNotSet = errors.New("log service name must be set") + // ExitOnFatal defines whether to exit on fatal errors, defined here to make it easier to test. + ExitOnFatal = syncx.ForAtomicBool(true) + + truncatedField = Field(truncatedKey, true) +) diff --git a/pkg/logger/writer.go b/pkg/logger/writer.go new file mode 100644 index 0000000..4f7f97d --- /dev/null +++ b/pkg/logger/writer.go @@ -0,0 +1,491 @@ +package logger + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "path" + "runtime/debug" + "sync" + "sync/atomic" + + fatihcolor "github.com/fatih/color" + "github.com/perfect-panel/ppanel-server/pkg/color" + "github.com/perfect-panel/ppanel-server/pkg/errorx" +) + +type ( + Writer interface { + Alert(v any) + Close() error + Debug(v any, fields ...LogField) + Error(v any, fields ...LogField) + Info(v any, fields ...LogField) + Severe(v any) + Slow(v any, fields ...LogField) + Stack(v any) + Stat(v any, fields ...LogField) + } + + atomicWriter struct { + writer Writer + lock sync.RWMutex + } + + comboWriter struct { + writers []Writer + } + + concreteWriter struct { + infoLog io.WriteCloser + errorLog io.WriteCloser + severeLog io.WriteCloser + slowLog io.WriteCloser + statLog io.WriteCloser + stackLog io.Writer + } +) + +// NewWriter creates a new Writer with the given io.Writer. +func NewWriter(w io.Writer) Writer { + lw := newLogWriter(log.New(w, "", flags)) + + return &concreteWriter{ + infoLog: lw, + errorLog: lw, + severeLog: lw, + slowLog: lw, + statLog: lw, + stackLog: lw, + } +} + +func (w *atomicWriter) Load() Writer { + w.lock.RLock() + defer w.lock.RUnlock() + return w.writer +} + +func (w *atomicWriter) Store(v Writer) { + w.lock.Lock() + defer w.lock.Unlock() + w.writer = v +} + +func (w *atomicWriter) StoreIfNil(v Writer) Writer { + w.lock.Lock() + defer w.lock.Unlock() + + if w.writer == nil { + w.writer = v + } + + return w.writer +} + +func (w *atomicWriter) Swap(v Writer) Writer { + w.lock.Lock() + defer w.lock.Unlock() + old := w.writer + w.writer = v + return old +} + +func (c comboWriter) Alert(v any) { + for _, w := range c.writers { + w.Alert(v) + } +} + +func (c comboWriter) Close() error { + var be errorx.BatchError + for _, w := range c.writers { + be.Add(w.Close()) + } + return be.Err() +} + +func (c comboWriter) Debug(v any, fields ...LogField) { + for _, w := range c.writers { + w.Debug(v, fields...) + } +} + +func (c comboWriter) Error(v any, fields ...LogField) { + for _, w := range c.writers { + w.Error(v, fields...) + } +} + +func (c comboWriter) Info(v any, fields ...LogField) { + for _, w := range c.writers { + w.Info(v, fields...) + } +} + +func (c comboWriter) Severe(v any) { + for _, w := range c.writers { + w.Severe(v) + } +} + +func (c comboWriter) Slow(v any, fields ...LogField) { + for _, w := range c.writers { + w.Slow(v, fields...) + } +} + +func (c comboWriter) Stack(v any) { + for _, w := range c.writers { + w.Stack(v) + } +} + +func (c comboWriter) Stat(v any, fields ...LogField) { + for _, w := range c.writers { + w.Stat(v, fields...) + } +} + +func newConsoleWriter() Writer { + outLog := newLogWriter(log.New(fatihcolor.Output, "", flags)) + errLog := newLogWriter(log.New(fatihcolor.Error, "", flags)) + return &concreteWriter{ + infoLog: outLog, + errorLog: errLog, + severeLog: errLog, + slowLog: errLog, + stackLog: newLessWriter(errLog, options.logStackCooldownMills), + statLog: outLog, + } +} + +func newFileWriter(c LogConf) (Writer, error) { + var err error + var opts []LogOption + var infoLog io.WriteCloser + var errorLog io.WriteCloser + var severeLog io.WriteCloser + var slowLog io.WriteCloser + var statLog io.WriteCloser + var stackLog io.Writer + + if len(c.Path) == 0 { + return nil, ErrLogPathNotSet + } + + opts = append(opts, WithCooldownMillis(c.StackCooldownMillis)) + if c.Compress { + opts = append(opts, WithGzip()) + } + if c.KeepDays > 0 { + opts = append(opts, WithKeepDays(c.KeepDays)) + } + if c.MaxBackups > 0 { + opts = append(opts, WithMaxBackups(c.MaxBackups)) + } + if c.MaxSize > 0 { + opts = append(opts, WithMaxSize(c.MaxSize)) + } + + opts = append(opts, WithRotation(c.Rotation)) + + accessFile := path.Join(c.Path, accessFilename) + errorFile := path.Join(c.Path, errorFilename) + severeFile := path.Join(c.Path, severeFilename) + slowFile := path.Join(c.Path, slowFilename) + statFile := path.Join(c.Path, statFilename) + + handleOptions(opts) + setupLogLevel(c) + + if infoLog, err = createOutput(accessFile); err != nil { + return nil, err + } + + if errorLog, err = createOutput(errorFile); err != nil { + return nil, err + } + + if severeLog, err = createOutput(severeFile); err != nil { + return nil, err + } + + if slowLog, err = createOutput(slowFile); err != nil { + return nil, err + } + + if statLog, err = createOutput(statFile); err != nil { + return nil, err + } + + stackLog = newLessWriter(errorLog, options.logStackCooldownMills) + + return &concreteWriter{ + infoLog: infoLog, + errorLog: errorLog, + severeLog: severeLog, + slowLog: slowLog, + statLog: statLog, + stackLog: stackLog, + }, nil +} + +func (w *concreteWriter) Alert(v any) { + output(w.errorLog, levelAlert, v) +} + +func (w *concreteWriter) Close() error { + if err := w.infoLog.Close(); err != nil { + return err + } + + if err := w.errorLog.Close(); err != nil { + return err + } + + if err := w.severeLog.Close(); err != nil { + return err + } + + if err := w.slowLog.Close(); err != nil { + return err + } + + return w.statLog.Close() +} + +func (w *concreteWriter) Debug(v any, fields ...LogField) { + output(w.infoLog, levelDebug, v, fields...) +} + +func (w *concreteWriter) Error(v any, fields ...LogField) { + output(w.errorLog, levelError, v, fields...) +} + +func (w *concreteWriter) Info(v any, fields ...LogField) { + output(w.infoLog, levelInfo, v, fields...) +} + +func (w *concreteWriter) Severe(v any) { + output(w.severeLog, levelFatal, v) +} + +func (w *concreteWriter) Slow(v any, fields ...LogField) { + output(w.slowLog, levelSlow, v, fields...) +} + +func (w *concreteWriter) Stack(v any) { + output(w.stackLog, levelError, v) +} + +func (w *concreteWriter) Stat(v any, fields ...LogField) { + output(w.statLog, levelStat, v, fields...) +} + +type nopWriter struct{} + +func (n nopWriter) Alert(_ any) { +} + +func (n nopWriter) Close() error { + return nil +} + +func (n nopWriter) Debug(_ any, _ ...LogField) { +} + +func (n nopWriter) Error(_ any, _ ...LogField) { +} + +func (n nopWriter) Info(_ any, _ ...LogField) { +} + +func (n nopWriter) Severe(_ any) { +} + +func (n nopWriter) Slow(_ any, _ ...LogField) { +} + +func (n nopWriter) Stack(_ any) { +} + +func (n nopWriter) Stat(_ any, _ ...LogField) { +} + +func buildPlainFields(fields logEntry) []string { + orderedKeys := []string{"duration", "caller"} + items := make([]string, 0, len(fields)) + for _, key := range orderedKeys { + // append the keys that have been set + if value, exists := fields[key]; exists { + items = append(items, fmt.Sprintf("%s=%+v", key, value)) + } + } + for key, value := range fields { + if !contains(orderedKeys, key) { // skip the keys that have been appended + items = append(items, fmt.Sprintf("%s=%+v", key, value)) + } + } + return items +} + +func combineGlobalFields(fields []LogField) []LogField { + globals := globalFields.Load() + if globals == nil { + return fields + } + + gf := globals.([]LogField) + ret := make([]LogField, 0, len(gf)+len(fields)) + ret = append(ret, gf...) + ret = append(ret, fields...) + + return ret +} + +func marshalJson(t interface{}) ([]byte, error) { + var buf bytes.Buffer + encoder := json.NewEncoder(&buf) + encoder.SetEscapeHTML(false) + err := encoder.Encode(t) + // go 1.5+ will append a newline to the end of the json string + // https://github.com/golang/go/issues/13520 + if l := buf.Len(); l > 0 && buf.Bytes()[l-1] == '\n' { + buf.Truncate(l - 1) + } + + return buf.Bytes(), err +} + +func output(writer io.Writer, level string, val any, fields ...LogField) { + // only truncate string content, don't know how to truncate the values of other types. + if v, ok := val.(string); ok { + maxLen := atomic.LoadUint32(&maxContentLength) + if maxLen > 0 && len(v) > int(maxLen) { + val = v[:maxLen] + fields = append(fields, truncatedField) + } + } + + fields = combineGlobalFields(fields) + // +3 for timestamp, level and content + entry := make(logEntry, len(fields)+3) + for _, field := range fields { + entry[field.Key] = field.Value + } + + switch atomic.LoadUint32(&encoding) { + case plainEncodingType: + plainFields := buildPlainFields(entry) + writePlainAny(writer, level, val, plainFields...) + default: + entry[timestampKey] = getTimestamp() + entry[levelKey] = level + entry[contentKey] = val + writeJson(writer, entry) + } +} + +func wrapLevelWithColor(level string) string { + var colour color.Color + switch level { + case levelAlert: + colour = color.FgRed + case levelError: + colour = color.FgRed + case levelFatal: + colour = color.FgRed + case levelInfo: + colour = color.FgBlue + case levelSlow: + colour = color.FgYellow + case levelDebug: + colour = color.FgYellow + case levelStat: + colour = color.FgGreen + } + + if colour == color.NoColor { + return level + } + + return color.WithColorPadding(level, colour) +} + +func writeJson(writer io.Writer, info any) { + if content, err := marshalJson(info); err != nil { + log.Printf("err: %s\n\n%s", err.Error(), debug.Stack()) + } else if writer == nil { + log.Println(string(content)) + } else { + if _, err := writer.Write(append(content, '\n')); err != nil { + log.Println(err.Error()) + } + } +} + +func writePlainAny(writer io.Writer, level string, val any, fields ...string) { + level = wrapLevelWithColor(level) + + switch v := val.(type) { + case string: + writePlainText(writer, level, v, fields...) + case error: + writePlainText(writer, level, v.Error(), fields...) + case fmt.Stringer: + writePlainText(writer, level, v.String(), fields...) + default: + writePlainValue(writer, level, v, fields...) + } +} + +func writePlainText(writer io.Writer, level, msg string, fields ...string) { + var buf bytes.Buffer + buf.WriteString(getTimestamp()) + buf.WriteByte(plainEncodingSep) + buf.WriteString(level) + buf.WriteByte(plainEncodingSep) + buf.WriteString(msg) + for _, item := range fields { + buf.WriteByte(plainEncodingSep) + buf.WriteString(item) + } + buf.WriteByte('\n') + if writer == nil { + log.Println(buf.String()) + return + } + + if _, err := writer.Write(buf.Bytes()); err != nil { + log.Println(err.Error()) + } +} + +func writePlainValue(writer io.Writer, level string, val any, fields ...string) { + var buf bytes.Buffer + buf.WriteString(getTimestamp()) + buf.WriteByte(plainEncodingSep) + buf.WriteString(level) + buf.WriteByte(plainEncodingSep) + if err := json.NewEncoder(&buf).Encode(val); err != nil { + log.Printf("err: %s\n\n%s", err.Error(), debug.Stack()) + return + } + + for _, item := range fields { + buf.WriteByte(plainEncodingSep) + buf.WriteString(item) + } + buf.WriteByte('\n') + if writer == nil { + log.Println(buf.String()) + return + } + + if _, err := writer.Write(buf.Bytes()); err != nil { + log.Println(err.Error()) + } +} diff --git a/pkg/logger/writer_test.go b/pkg/logger/writer_test.go new file mode 100644 index 0000000..8138230 --- /dev/null +++ b/pkg/logger/writer_test.go @@ -0,0 +1,440 @@ +package logger + +import ( + "bytes" + "encoding/json" + "errors" + "log" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestNewWriter(t *testing.T) { + const literal = "foo bar" + var buf bytes.Buffer + w := NewWriter(&buf) + w.Info(literal) + assert.Contains(t, buf.String(), literal) + buf.Reset() + w.Debug(literal) + assert.Contains(t, buf.String(), literal) +} + +func TestConsoleWriter(t *testing.T) { + var buf bytes.Buffer + w := newConsoleWriter() + lw := newLogWriter(log.New(&buf, "", 0)) + w.(*concreteWriter).errorLog = lw + w.Alert("foo bar 1") + var val mockedEntry + if err := json.Unmarshal(buf.Bytes(), &val); err != nil { + t.Fatal(err) + } + assert.Equal(t, levelAlert, val.Level) + assert.Equal(t, "foo bar 1", val.Content) + + buf.Reset() + w.(*concreteWriter).errorLog = lw + w.Error("foo bar 2") + if err := json.Unmarshal(buf.Bytes(), &val); err != nil { + t.Fatal(err) + } + assert.Equal(t, levelError, val.Level) + assert.Equal(t, "foo bar 2", val.Content) + + buf.Reset() + w.(*concreteWriter).infoLog = lw + w.Info("foo bar 3") + if err := json.Unmarshal(buf.Bytes(), &val); err != nil { + t.Fatal(err) + } + assert.Equal(t, levelInfo, val.Level) + assert.Equal(t, "foo bar 3", val.Content) + + buf.Reset() + w.(*concreteWriter).severeLog = lw + w.Severe("foo bar 4") + if err := json.Unmarshal(buf.Bytes(), &val); err != nil { + t.Fatal(err) + } + assert.Equal(t, levelFatal, val.Level) + assert.Equal(t, "foo bar 4", val.Content) + + buf.Reset() + w.(*concreteWriter).slowLog = lw + w.Slow("foo bar 5") + if err := json.Unmarshal(buf.Bytes(), &val); err != nil { + t.Fatal(err) + } + assert.Equal(t, levelSlow, val.Level) + assert.Equal(t, "foo bar 5", val.Content) + + buf.Reset() + w.(*concreteWriter).statLog = lw + w.Stat("foo bar 6") + if err := json.Unmarshal(buf.Bytes(), &val); err != nil { + t.Fatal(err) + } + assert.Equal(t, levelStat, val.Level) + assert.Equal(t, "foo bar 6", val.Content) + + w.(*concreteWriter).infoLog = hardToCloseWriter{} + assert.NotNil(t, w.Close()) + w.(*concreteWriter).infoLog = easyToCloseWriter{} + w.(*concreteWriter).errorLog = hardToCloseWriter{} + assert.NotNil(t, w.Close()) + w.(*concreteWriter).errorLog = easyToCloseWriter{} + w.(*concreteWriter).severeLog = hardToCloseWriter{} + assert.NotNil(t, w.Close()) + w.(*concreteWriter).severeLog = easyToCloseWriter{} + w.(*concreteWriter).slowLog = hardToCloseWriter{} + assert.NotNil(t, w.Close()) + w.(*concreteWriter).slowLog = easyToCloseWriter{} + w.(*concreteWriter).statLog = hardToCloseWriter{} + assert.NotNil(t, w.Close()) + w.(*concreteWriter).statLog = easyToCloseWriter{} +} + +func TestNewFileWriter(t *testing.T) { + t.Run("access", func(t *testing.T) { + _, err := newFileWriter(LogConf{ + Path: "/not-exists", + }) + assert.Error(t, err) + }) +} + +func TestNopWriter(t *testing.T) { + assert.NotPanics(t, func() { + var w nopWriter + w.Alert("foo") + w.Debug("foo") + w.Error("foo") + w.Info("foo") + w.Severe("foo") + w.Stack("foo") + w.Stat("foo") + w.Slow("foo") + _ = w.Close() + }) +} + +func TestWriteJson(t *testing.T) { + var buf bytes.Buffer + log.SetOutput(&buf) + writeJson(nil, "foo") + assert.Contains(t, buf.String(), "foo") + + buf.Reset() + writeJson(hardToWriteWriter{}, "foo") + assert.Contains(t, buf.String(), "write error") + + buf.Reset() + writeJson(nil, make(chan int)) + assert.Contains(t, buf.String(), "unsupported type") + + buf.Reset() + type C struct { + RC func() + } + writeJson(nil, C{ + RC: func() {}, + }) + assert.Contains(t, buf.String(), "runtime/debug.Stack") +} + +func TestWritePlainAny(t *testing.T) { + var buf bytes.Buffer + log.SetOutput(&buf) + writePlainAny(nil, levelInfo, "foo") + assert.Contains(t, buf.String(), "foo") + + buf.Reset() + writePlainAny(nil, levelDebug, make(chan int)) + assert.Contains(t, buf.String(), "unsupported type") + writePlainAny(nil, levelDebug, 100) + assert.Contains(t, buf.String(), "100") + + buf.Reset() + writePlainAny(nil, levelError, make(chan int)) + assert.Contains(t, buf.String(), "unsupported type") + writePlainAny(nil, levelSlow, 100) + assert.Contains(t, buf.String(), "100") + + buf.Reset() + writePlainAny(hardToWriteWriter{}, levelStat, 100) + assert.Contains(t, buf.String(), "write error") + + buf.Reset() + writePlainAny(hardToWriteWriter{}, levelSevere, "foo") + assert.Contains(t, buf.String(), "write error") + + buf.Reset() + writePlainAny(hardToWriteWriter{}, levelAlert, "foo") + assert.Contains(t, buf.String(), "write error") + + buf.Reset() + writePlainAny(hardToWriteWriter{}, levelFatal, "foo") + assert.Contains(t, buf.String(), "write error") + + buf.Reset() + type C struct { + RC func() + } + writePlainAny(nil, levelError, C{ + RC: func() {}, + }) + assert.Contains(t, buf.String(), "runtime/debug.Stack") +} + +func TestWritePlainDuplicate(t *testing.T) { + old := atomic.SwapUint32(&encoding, plainEncodingType) + t.Cleanup(func() { + atomic.StoreUint32(&encoding, old) + }) + + var buf bytes.Buffer + output(&buf, levelInfo, "foo", LogField{ + Key: "first", + Value: "a", + }, LogField{ + Key: "first", + Value: "b", + }) + assert.Contains(t, buf.String(), "foo") + assert.NotContains(t, buf.String(), "first=a") + assert.Contains(t, buf.String(), "first=b") + + buf.Reset() + output(&buf, levelInfo, "foo", LogField{ + Key: "first", + Value: "a", + }, LogField{ + Key: "first", + Value: "b", + }, LogField{ + Key: "second", + Value: "c", + }) + assert.Contains(t, buf.String(), "foo") + assert.NotContains(t, buf.String(), "first=a") + assert.Contains(t, buf.String(), "first=b") + assert.Contains(t, buf.String(), "second=c") +} + +func TestLogWithLimitContentLength(t *testing.T) { + maxLen := atomic.LoadUint32(&maxContentLength) + atomic.StoreUint32(&maxContentLength, 10) + + t.Cleanup(func() { + atomic.StoreUint32(&maxContentLength, maxLen) + }) + + t.Run("alert", func(t *testing.T) { + var buf bytes.Buffer + w := NewWriter(&buf) + w.Info("1234567890") + var v1 mockedEntry + if err := json.Unmarshal(buf.Bytes(), &v1); err != nil { + t.Fatal(err) + } + assert.Equal(t, "1234567890", v1.Content) + assert.False(t, v1.Truncated) + + buf.Reset() + var v2 mockedEntry + w.Info("12345678901") + if err := json.Unmarshal(buf.Bytes(), &v2); err != nil { + t.Fatal(err) + } + assert.Equal(t, "1234567890", v2.Content) + assert.True(t, v2.Truncated) + }) +} + +func TestComboWriter(t *testing.T) { + var mockWriters []Writer + for i := 0; i < 3; i++ { + mockWriters = append(mockWriters, new(tracedWriter)) + } + + cw := comboWriter{ + writers: mockWriters, + } + + t.Run("Alert", func(t *testing.T) { + for _, mw := range cw.writers { + mw.(*tracedWriter).On("Alert", "test alert").Once() + } + cw.Alert("test alert") + for _, mw := range cw.writers { + mw.(*tracedWriter).AssertCalled(t, "Alert", "test alert") + } + }) + + t.Run("Close", func(t *testing.T) { + for i := range cw.writers { + if i == 1 { + cw.writers[i].(*tracedWriter).On("Close").Return(errors.New("error")).Once() + } else { + cw.writers[i].(*tracedWriter).On("Close").Return(nil).Once() + } + } + err := cw.Close() + assert.Error(t, err) + for _, mw := range cw.writers { + mw.(*tracedWriter).AssertCalled(t, "Close") + } + }) + + t.Run("Debug", func(t *testing.T) { + fields := []LogField{{Key: "key", Value: "value"}} + for _, mw := range cw.writers { + mw.(*tracedWriter).On("Debug", "test debug", fields).Once() + } + cw.Debug("test debug", fields...) + for _, mw := range cw.writers { + mw.(*tracedWriter).AssertCalled(t, "Debug", "test debug", fields) + } + }) + + t.Run("Error", func(t *testing.T) { + fields := []LogField{{Key: "key", Value: "value"}} + for _, mw := range cw.writers { + mw.(*tracedWriter).On("Error", "test error", fields).Once() + } + cw.Error("test error", fields...) + for _, mw := range cw.writers { + mw.(*tracedWriter).AssertCalled(t, "Error", "test error", fields) + } + }) + + t.Run("Info", func(t *testing.T) { + fields := []LogField{{Key: "key", Value: "value"}} + for _, mw := range cw.writers { + mw.(*tracedWriter).On("Info", "test info", fields).Once() + } + cw.Info("test info", fields...) + for _, mw := range cw.writers { + mw.(*tracedWriter).AssertCalled(t, "Info", "test info", fields) + } + }) + + t.Run("Severe", func(t *testing.T) { + for _, mw := range cw.writers { + mw.(*tracedWriter).On("Severe", "test severe").Once() + } + cw.Severe("test severe") + for _, mw := range cw.writers { + mw.(*tracedWriter).AssertCalled(t, "Severe", "test severe") + } + }) + + t.Run("Slow", func(t *testing.T) { + fields := []LogField{{Key: "key", Value: "value"}} + for _, mw := range cw.writers { + mw.(*tracedWriter).On("Slow", "test slow", fields).Once() + } + cw.Slow("test slow", fields...) + for _, mw := range cw.writers { + mw.(*tracedWriter).AssertCalled(t, "Slow", "test slow", fields) + } + }) + + t.Run("Stack", func(t *testing.T) { + for _, mw := range cw.writers { + mw.(*tracedWriter).On("Stack", "test stack").Once() + } + cw.Stack("test stack") + for _, mw := range cw.writers { + mw.(*tracedWriter).AssertCalled(t, "Stack", "test stack") + } + }) + + t.Run("Stat", func(t *testing.T) { + fields := []LogField{{Key: "key", Value: "value"}} + for _, mw := range cw.writers { + mw.(*tracedWriter).On("Stat", "test stat", fields).Once() + } + cw.Stat("test stat", fields...) + for _, mw := range cw.writers { + mw.(*tracedWriter).AssertCalled(t, "Stat", "test stat", fields) + } + }) +} + +type mockedEntry struct { + Level string `json:"level"` + Content string `json:"content"` + Truncated bool `json:"truncated"` +} + +type easyToCloseWriter struct{} + +func (h easyToCloseWriter) Write(_ []byte) (_ int, _ error) { + return +} + +func (h easyToCloseWriter) Close() error { + return nil +} + +type hardToCloseWriter struct{} + +func (h hardToCloseWriter) Write(_ []byte) (_ int, _ error) { + return +} + +func (h hardToCloseWriter) Close() error { + return errors.New("close error") +} + +type hardToWriteWriter struct{} + +func (h hardToWriteWriter) Write(_ []byte) (_ int, _ error) { + return 0, errors.New("write error") +} + +type tracedWriter struct { + mock.Mock +} + +func (w *tracedWriter) Alert(v any) { + w.Called(v) +} + +func (w *tracedWriter) Close() error { + args := w.Called() + return args.Error(0) +} + +func (w *tracedWriter) Debug(v any, fields ...LogField) { + w.Called(v, fields) +} + +func (w *tracedWriter) Error(v any, fields ...LogField) { + w.Called(v, fields) +} + +func (w *tracedWriter) Info(v any, fields ...LogField) { + w.Called(v, fields) +} + +func (w *tracedWriter) Severe(v any) { + w.Called(v) +} + +func (w *tracedWriter) Slow(v any, fields ...LogField) { + w.Called(v, fields) +} + +func (w *tracedWriter) Stack(v any) { + w.Called(v) +} + +func (w *tracedWriter) Stat(v any, fields ...LogField) { + w.Called(v, fields) +} diff --git a/pkg/md5/md5.go b/pkg/md5/md5.go new file mode 100644 index 0000000..c8333a2 --- /dev/null +++ b/pkg/md5/md5.go @@ -0,0 +1,12 @@ +package md5 + +import ( + "crypto/md5" + "encoding/hex" +) + +func Sign(content string) string { + h := md5.New() + h.Write([]byte(content)) + return hex.EncodeToString(h.Sum(nil)) +} diff --git a/pkg/nodeMultiplier/manage_test.go b/pkg/nodeMultiplier/manage_test.go new file mode 100644 index 0000000..789dae9 --- /dev/null +++ b/pkg/nodeMultiplier/manage_test.go @@ -0,0 +1,22 @@ +package nodeMultiplier + +import ( + "testing" + "time" +) + +func TestNewNodeMultiplierManager(t *testing.T) { + periods := []TimePeriod{ + { + StartTime: "23:00", + EndTime: "1:59", + Multiplier: 1.2, + }, + } + m := NewNodeMultiplierManager(periods) + if len(m.Periods) != 1 { + t.Errorf("expected 1, got %d", len(m.Periods)) + } + + t.Log("00:10 multiplier:", m.GetMultiplier(time.Date(0, 1, 1, 0, 10, 0, 0, time.UTC))) +} diff --git a/pkg/nodeMultiplier/manager.go b/pkg/nodeMultiplier/manager.go new file mode 100644 index 0000000..7f9e687 --- /dev/null +++ b/pkg/nodeMultiplier/manager.go @@ -0,0 +1,43 @@ +package nodeMultiplier + +import "time" + +type TimePeriod struct { + StartTime string `json:"start_time"` + EndTime string `json:"end_time"` + Multiplier float32 `json:"multiplier"` +} + +type Manager struct { + Periods []TimePeriod +} + +func NewNodeMultiplierManager(periods []TimePeriod) *Manager { + return &Manager{ + Periods: periods, + } +} + +func (m *Manager) GetMultiplier(current time.Time) float32 { + for _, period := range m.Periods { + if m.isInTimePeriod(current, period.StartTime, period.EndTime) { + return period.Multiplier + } + } + return 1 // Default multiplier is 1 (no change) +} + +func (m *Manager) isInTimePeriod(current time.Time, start, end string) bool { + startTime, _ := time.Parse("15:04", start) + endTime, _ := time.Parse("15:04", end) + + currentTime := time.Date(0, 1, 1, current.Hour(), current.Minute(), 0, 0, time.UTC) + startTimeFormatted := time.Date(0, 1, 1, startTime.Hour(), startTime.Minute(), 0, 0, time.UTC) + endTimeFormatted := time.Date(0, 1, 1, endTime.Hour(), endTime.Minute(), 0, 0, time.UTC) + + if startTimeFormatted.Before(endTimeFormatted) { + return currentTime.After(startTimeFormatted) && currentTime.Before(endTimeFormatted) + } + // Handle ranges that cross midnight + return currentTime.After(startTimeFormatted) || currentTime.Before(endTimeFormatted) +} diff --git a/pkg/oauth/apple/apple.html b/pkg/oauth/apple/apple.html new file mode 100644 index 0000000..aa77fc1 --- /dev/null +++ b/pkg/oauth/apple/apple.html @@ -0,0 +1,21 @@ + + + + + Apple 登录 + + +
+ + + + + \ No newline at end of file diff --git a/pkg/oauth/apple/apple_test.go b/pkg/oauth/apple/apple_test.go new file mode 100644 index 0000000..511bc6f --- /dev/null +++ b/pkg/oauth/apple/apple_test.go @@ -0,0 +1,76 @@ +package apple + +import ( + "context" + "fmt" + "log" + "net/http" + "testing" + + "github.com/gin-gonic/gin" +) + +func TestAppleLogin(t *testing.T) { + t.Skipf("Skip TestAppleLogin test") + router := gin.Default() + router.LoadHTMLGlob("./*") + router.GET("/apple", func(c *gin.Context) { + c.HTML(http.StatusOK, "apple.html", gin.H{ + "title": "Gin HTML Example", + "message": "Hello, Gin!", + }) + }) + router.POST("/auth/apple/callback", func(c *gin.Context) { + var req CallbackRequest + if err := c.ShouldBind(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request data"}) + return + } + handleAppleCallBack(c, req) + }) + _ = router.RunTLS(":8443", "certificate.crt", "private.key") +} + +func handleAppleCallBack(ctx context.Context, request CallbackRequest) { + fmt.Printf("request: %+v\n", request) + // validate the token + client, err := New(Config{ + TeamID: TeamID, + ClientID: ClientID, + KeyID: KeyID, + ClientSecret: ClientSecret, + RedirectURI: "https://test.muran.org:8443/auth/apple/callback", + }) + if err != nil { + fmt.Println("error creating apple client: " + err.Error()) + return + } + resp, err := client.VerifyWebToken(ctx, request.Code) + if err != nil { + fmt.Println("error verifying token: " + err.Error()) + return + } + if resp.Error != "" { + fmt.Printf("apple returned an error: %s - %s\n", resp.Error, resp.ErrorDescription) + return + } + + // Get the unique user ID + unique, err := GetUniqueID(resp.IDToken) + if err != nil { + fmt.Println("error getting unique id: " + err.Error()) + return + } + // Get the email + claim, err := GetClaims(resp.IDToken) + if err != nil { + fmt.Println("failed to get claims: " + err.Error()) + return + } + email := (*claim)["email"] + emailVerified := (*claim)["email_verified"] + isPrivateEmail := (*claim)["is_private_email"] + + // Voila! + log.Printf("\n unique: %s \n email: %s \n email_verified: %v \n is_private_email: %v", unique, email, emailVerified, isPrivateEmail) +} diff --git a/pkg/oauth/apple/client.go b/pkg/oauth/apple/client.go new file mode 100644 index 0000000..abb9e8c --- /dev/null +++ b/pkg/oauth/apple/client.go @@ -0,0 +1,34 @@ +package apple + +import ( + "fmt" + "net/http" + "time" +) + +type Config struct { + TeamID string + ClientID string + KeyID string + ClientSecret string + RedirectURI string +} + +// New creates a Client object with the default URLs and a default http client +func New(c Config) (*Client, error) { + fmt.Printf("config: %+v\n", c) + secret, err := GenerateClientSecret(c.ClientSecret, c.TeamID, c.ClientID, c.KeyID) + if err != nil { + fmt.Println("error generating secret: " + err.Error()) + return nil, err + } + return &Client{ + config: c, + validationURL: ValidationURL, + revokeURL: RevokeURL, + client: &http.Client{ + Timeout: 5 * time.Second, + }, + secret: secret, + }, nil +} diff --git a/pkg/oauth/apple/model.go b/pkg/oauth/apple/model.go new file mode 100644 index 0000000..7a27f72 --- /dev/null +++ b/pkg/oauth/apple/model.go @@ -0,0 +1,134 @@ +package apple + +// WebValidationTokenRequest is based off of https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens +type WebValidationTokenRequest struct { + // ClientID is the "Services ID" value that you get when navigating to your "sign in with Apple"-enabled service ID + ClientID string + + // ClientSecret is secret generated as a JSON Web Token that uses the secret key generated by the WWDR portal. + // It can also be generated using the GenerateClientSecret function provided in this package + ClientSecret string + + // Code is the authorization code received from your application’s user agent. + // The code is single use only and valid for five minutes. + Code string + + // RedirectURI is the destination URI the code was originally sent to. + // Redirect URLs must be registered with Apple. You can register up to 10. Apple will throw an error with IP address + // URLs on the authorization screen, and will not let you add localhost in the developer portal. + RedirectURI string +} + +// CallbackRequest Apple Callback Request +type CallbackRequest struct { + // Code is the authorization code received from your application’s user agent. + // The code is single use only and valid for five minutes. + Code string `form:"code"` + IdToken string `form:"id_token"` + State string `form:"state"` +} + +// AppValidationTokenRequest is based off of https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens +type AppValidationTokenRequest struct { + // ClientID is the package name of your app + ClientID string + + // ClientSecret is secret generated as a JSON Web Token that uses the secret key generated by the WWDR portal. + // It can also be generated using the GenerateClientSecret function provided in this package + ClientSecret string + + // The authorization code received in an authorization response sent to your app. The code is single-use only and valid for five minutes. + // Authorization code validation requests require this parameter. + Code string +} + +// ValidationRefreshRequest is based off of https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens +type ValidationRefreshRequest struct { + // ClientID is the "Services ID" value that you get when navigating to your "sign in with Apple"-enabled service ID + ClientID string + + // ClientSecret is secret generated as a JSON Web Token that uses the secret key generated by the WWDR portal. + // It can also be generated using the GenerateClientSecret function provided in this package + ClientSecret string + + // RefreshToken is the refresh token given during a previous validation + RefreshToken string +} + +// RevokeAccessTokenRequest is based off https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens +type RevokeAccessTokenRequest struct { + // ClientID is the "Services ID" value that you get when navigating to your "sign in with Apple"-enabled service ID + ClientID string + + // ClientSecret is secret generated as a JSON Web Token that uses the secret key generated by the WWDR portal. + // It can also be generated using the GenerateClientSecret function provided in this package + ClientSecret string + + // AccessToken is the auth token given during a previous validation + AccessToken string +} + +// RevokeRefreshTokenRequest is based off https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens +type RevokeRefreshTokenRequest struct { + // ClientID is the "Services ID" value that you get when navigating to your "sign in with Apple"-enabled service ID + ClientID string + + // ClientSecret is secret generated as a JSON Web Token that uses the secret key generated by the WWDR portal. + // It can also be generated using the GenerateClientSecret function provided in this package + ClientSecret string + + // RefreshToken is the refresh token given during a previous validation + RefreshToken string +} + +// ValidationResponse is based off of https://developer.apple.com/documentation/signinwithapplerestapi/tokenresponse +type ValidationResponse struct { + // (Reserved for future use) A token used to access allowed data. Currently, no data set has been defined for access. + AccessToken string `json:"access_token"` + + // The type of access token. It will always be "bearer". + TokenType string `json:"token_type"` + + // The amount of time, in seconds, before the access token expires. You can revalidate with the "RefreshToken" + ExpiresIn int `json:"expires_in"` + + // The refresh token used to regenerate new access tokens. Store this token securely on your server. + // The refresh token isn’t returned when validating an existing refresh token. Please refer to RefreshReponse below + RefreshToken string `json:"refresh_token"` + + // A JSON Web Token that contains the user’s identity information. + IDToken string `json:"id_token"` + + // Used to capture any error returned by the endpoint. Do not trust the response if this error is not nil + Error string `json:"error"` + + // A more detailed precision about the current error. + ErrorDescription string `json:"error_description"` +} + +// RefreshResponse is a subset of ValidationResponse returned by Apple +type RefreshResponse struct { + // (Reserved for future use) A token used to access allowed data. Currently, no data set has been defined for access. + AccessToken string `json:"access_token"` + + // The type of access token. It will always be "bearer". + TokenType string `json:"token_type"` + + // The amount of time, in seconds, before the access token expires. You can revalidate with this token + ExpiresIn int `json:"expires_in"` + + // Used to capture any error returned by the endpoint. Do not trust the response if this error is not nil + Error string `json:"error"` + + // A more detailed precision about the current error. + ErrorDescription string `json:"error_description"` +} + +// RevokeResponse is based of https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens +type RevokeResponse struct { + // Used to capture any error returned by the endpoint + Error string `json:"error"` + + // A more detailed precision about the current error. + ErrorDescription string `json:"error_description"` +} diff --git a/pkg/oauth/apple/secret.go b/pkg/oauth/apple/secret.go new file mode 100644 index 0000000..d8fea7a --- /dev/null +++ b/pkg/oauth/apple/secret.go @@ -0,0 +1,53 @@ +package apple + +import ( + "crypto/x509" + "encoding/pem" + "errors" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +/* +GenerateClientSecret generates the client secret used to make requests to the validation server. +The secret expires after 6 months + +signingKey - Private key from Apple obtained by going to the keys section of the developer section +teamID - Your 10-character Team ID +clientID - Your Services ID, e.g. com.aaronparecki.services +keyID - Find the 10-char Key ID value from the portal +*/ +func GenerateClientSecret(signingKey, teamID, clientID, keyID string) (string, error) { + block, _ := pem.Decode([]byte(signingKey)) + if block == nil { + return "", errors.New("empty block after decoding") + } + + privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return "", err + } + + // Create the Claims + now := time.Now() + claims := &jwt.RegisteredClaims{ + Issuer: teamID, + IssuedAt: &jwt.NumericDate{ + Time: now, + }, + ExpiresAt: &jwt.NumericDate{ + Time: now.Add(time.Hour*24*180 - time.Second), // 180 days + }, + Audience: jwt.ClaimStrings{ + "https://appleid.apple.com", + }, + Subject: clientID, + } + + token := jwt.NewWithClaims(jwt.SigningMethodES256, claims) + token.Header["alg"] = "ES256" + token.Header["kid"] = keyID + + return token.SignedString(privateKey) +} diff --git a/pkg/oauth/apple/validator.go b/pkg/oauth/apple/validator.go new file mode 100644 index 0000000..5a88211 --- /dev/null +++ b/pkg/oauth/apple/validator.go @@ -0,0 +1,200 @@ +package apple + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "net/url" + "strings" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +const ( + // ValidationURL is the endpoint for verifying tokens + ValidationURL string = "https://appleid.apple.com/auth/token" + // RevokeURL is the endpoint for revoking tokens + RevokeURL string = "https://appleid.apple.com/auth/revoke" + // ContentType is the one expected by Apple + ContentType string = "application/x-www-form-urlencoded" + // UserAgent is required by Apple or the request will fail + UserAgent string = "go-signin-with-apple" + // AcceptHeader is the content that we are willing to accept + AcceptHeader string = "application/json" +) + +// ValidationClient is an interface to call the validation API +type ValidationClient interface { + VerifyWebToken(ctx context.Context, reqBody WebValidationTokenRequest, result interface{}) error + VerifyAppToken(ctx context.Context, reqBody AppValidationTokenRequest, result interface{}) error + VerifyRefreshToken(ctx context.Context, reqBody ValidationRefreshRequest, result interface{}) error + RevokeAccessToken(ctx context.Context, reqBody RevokeAccessTokenRequest, result interface{}) error + RevokeRefreshToken(ctx context.Context, reqBody RevokeRefreshTokenRequest, result interface{}) error +} + +// Client implements ValidationClient +type Client struct { + config Config + validationURL string + revokeURL string + secret string + client *http.Client +} + +// ClientOptions is a struct to hold the options for the client +type ClientOptions struct { + validationURL string + revokeURL string + //nolint:unused + secret string + client *http.Client +} + +// NewWithURL creates a Client object with a custom URL provided +// +// Deprecated: This function is deprecated and will be removed in a future version. Use NewWithOptions instead. +func NewWithURL(validationURL string, revokeURL string) *Client { + return NewWithOptions(ClientOptions{ + validationURL: validationURL, + revokeURL: revokeURL, + }) +} + +// NewWithOptions creates a Client object with custom options. It will default to the standard options if not provided +func NewWithOptions(options ClientOptions) *Client { + if options.client == nil { + options.client = &http.Client{ + Timeout: 5 * time.Second, + } + } + if options.validationURL == "" { + options.validationURL = ValidationURL + } + if options.revokeURL == "" { + options.revokeURL = RevokeURL + } + client := &Client{ + validationURL: options.validationURL, + revokeURL: options.revokeURL, + client: options.client, + } + return client +} + +// VerifyWebToken sends the WebValidationTokenRequest and gets validation result +func (c *Client) VerifyWebToken(ctx context.Context, code string) (ValidationResponse, error) { + data := url.Values{ + "client_id": {c.config.ClientID}, + "client_secret": {c.secret}, + "code": {code}, + "redirect_uri": {c.config.RedirectURI}, + "grant_type": {"authorization_code"}, + } + var resp ValidationResponse + err := doRequest(ctx, c.client, &resp, c.validationURL, data) + + return resp, err +} + +// VerifyAppToken sends the AppValidationTokenRequest and gets validation result +func (c *Client) VerifyAppToken(ctx context.Context, reqBody AppValidationTokenRequest, result interface{}) error { + data := url.Values{ + "client_id": {reqBody.ClientID}, + "client_secret": {reqBody.ClientSecret}, + "code": {reqBody.Code}, + "grant_type": {"authorization_code"}, + } + + return doRequest(ctx, c.client, &result, c.validationURL, data) +} + +// VerifyRefreshToken sends the WebValidationTokenRequest and gets validation result +func (c *Client) VerifyRefreshToken(ctx context.Context, reqBody ValidationRefreshRequest, result interface{}) error { + data := url.Values{ + "client_id": {reqBody.ClientID}, + "client_secret": {reqBody.ClientSecret}, + "refresh_token": {reqBody.RefreshToken}, + "grant_type": {"refresh_token"}, + } + + return doRequest(ctx, c.client, &result, c.validationURL, data) +} + +// RevokeRefreshToken revokes the Refresh Token and gets the revoke result +func (c *Client) RevokeRefreshToken(ctx context.Context, token string, result interface{}) error { + data := url.Values{ + "client_id": {c.config.ClientID}, + "client_secret": {c.secret}, + "token": {token}, + "token_type_hint": {"refresh_token"}, + } + + return doRequest(ctx, c.client, result, c.revokeURL, data) +} + +// RevokeAccessToken revokes the Access Token and gets the revoke result +func (c *Client) RevokeAccessToken(ctx context.Context, token string, result interface{}) error { + data := url.Values{ + "client_id": {c.config.ClientID}, + "client_secret": {c.secret}, + "token": {token}, + "token_type_hint": {"access_token"}, + } + log.Printf("revoke access token: %v", data) + return doRequest(ctx, c.client, &result, c.revokeURL, data) +} + +// GetUniqueID decodes the id_token response and returns the unique subject ID to identify the user +func GetUniqueID(idToken string) (string, error) { + token, _, err := new(jwt.Parser).ParseUnverified(idToken, jwt.MapClaims{}) + if err != nil { + return "", err + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return "", fmt.Errorf("invalid token claims") + } + + return fmt.Sprintf("%v", claims["sub"]), nil +} + +// GetClaims decodes the id_token response and returns the JWT claims to identify the user +func GetClaims(idToken string) (*jwt.MapClaims, error) { + token, _, err := new(jwt.Parser).ParseUnverified(idToken, jwt.MapClaims{}) + if err != nil { + return nil, err + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return nil, fmt.Errorf("invalid token claims") + } + + return &claims, nil +} + +func doRequest(ctx context.Context, client *http.Client, result interface{}, url string, data url.Values) error { + req, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(data.Encode())) + if err != nil { + return err + } + req.Header.Add("content-type", ContentType) + req.Header.Add("accept", AcceptHeader) + req.Header.Add("user-agent", UserAgent) // apple requires a user agent + + res, err := client.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + log.Printf("error response from apple: %s", res.Status) + } + + return json.NewDecoder(res.Body).Decode(result) +} diff --git a/pkg/oauth/google/google.go b/pkg/oauth/google/google.go new file mode 100644 index 0000000..1d5e724 --- /dev/null +++ b/pkg/oauth/google/google.go @@ -0,0 +1,61 @@ +package google + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" +) + +type Config struct { + ClientID string + ClientSecret string + RedirectURL string +} +type Client struct { + *oauth2.Config +} +type UserInfo struct { + OpenID string `json:"id"` + Email string `json:"email"` + Name string `json:"name"` + Picture string `json:"picture"` + VerifiedEmail bool `json:"verified_email"` +} + +func New(config *Config) *Client { + return &Client{ + &oauth2.Config{ + ClientID: config.ClientID, + ClientSecret: config.ClientSecret, + RedirectURL: config.RedirectURL, + Scopes: []string{"openid", "profile", "email", "https://www.googleapis.com/auth/user.phonenumbers.read"}, + Endpoint: google.Endpoint, + }, + } +} +func (c *Client) GetUserInfo(token string) (*UserInfo, error) { + client := c.Config.Client(context.Background(), &oauth2.Token{AccessToken: token}) + resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo") + if err != nil { + logger.Error("[Google OAuth 2.0] Get User Info", logger.Field("error", err.Error())) + return nil, err + } + defer resp.Body.Close() + + var userInfo map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil { + logger.Error("[Google OAuth 2.0] Decode User Info", logger.Field("error", err.Error())) + return nil, err + } + + return &UserInfo{ + OpenID: userInfo["id"].(string), + Email: userInfo["email"].(string), + Name: userInfo["name"].(string), + Picture: userInfo["picture"].(string), + VerifiedEmail: userInfo["verified_email"].(bool), + }, nil +} diff --git a/pkg/oauth/google/google_test.go b/pkg/oauth/google/google_test.go new file mode 100644 index 0000000..1d967d3 --- /dev/null +++ b/pkg/oauth/google/google_test.go @@ -0,0 +1,78 @@ +package google + +import ( + "context" + "fmt" + "log" + "net/http" + "testing" + + "golang.org/x/oauth2" +) + +func TestGoogleOAuth(t *testing.T) { + t.Skipf("Skip TestGoogleOAuth test") + http.HandleFunc("/", handleMain) + http.HandleFunc("/login", handleLogin) + http.HandleFunc("/auth", handleCallback) + http.HandleFunc("/user", handleAuth) + + fmt.Println("Server is running on http://localhost:3001") + log.Fatal(http.ListenAndServe(":3001", nil)) +} + +func handleMain(w http.ResponseWriter, r *http.Request) { + html := ` + + Log in with Google + + ` + fmt.Fprint(w, html) +} + +func handleLogin(w http.ResponseWriter, r *http.Request) { + oauthConfig := New(&Config{ + ClientID: "", + ClientSecret: "", + RedirectURL: "http://localhost:3001/auth", + }) + url := oauthConfig.AuthCodeURL("randomstate", oauth2.AccessTypeOffline) + http.Redirect(w, r, url, http.StatusTemporaryRedirect) +} + +func handleCallback(w http.ResponseWriter, r *http.Request) { + if r.FormValue("state") != "randomstate" { + http.Error(w, "State is invalid", http.StatusBadRequest) + return + } + + log.Printf("url: %v", r.URL) + + oauthConfig := New(&Config{ + ClientID: "", + ClientSecret: "Key", + RedirectURL: "http://localhost:3001/auth", + }) + code := r.FormValue("code") + token, err := oauthConfig.Exchange(context.Background(), code) + if err != nil { + http.Error(w, "Failed to exchange token", http.StatusInternalServerError) + return + } + http.Redirect(w, r, "/user?token="+token.AccessToken, http.StatusTemporaryRedirect) +} + +func handleAuth(w http.ResponseWriter, r *http.Request) { + token := r.FormValue("token") + client := New(&Config{ + ClientID: "Id", + ClientSecret: "Key", + RedirectURL: "http://localhost:3001/auth", + }) + userInfo, err := client.GetUserInfo(token) + if err != nil { + http.Error(w, "Failed to get user info", http.StatusInternalServerError) + return + } + fmt.Fprintf(w, "Hello, %s", userInfo.Name) +} diff --git a/pkg/oauth/telegram/client.go b/pkg/oauth/telegram/client.go new file mode 100644 index 0000000..8d4dde1 --- /dev/null +++ b/pkg/oauth/telegram/client.go @@ -0,0 +1,60 @@ +package telegram + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "net/url" + "strings" +) + +// ParseAuthDataJson parses provided json content for AuthData +func ParseAuthDataJson(content []byte) (*AuthData, error) { + data := &AuthData{} + err := json.Unmarshal(content, data) + if err != nil { + return nil, fmt.Errorf("unmarshaling error: %w", err) + } + return data, nil +} + +// ParseAuthDataBase64 decodes provided content from base64 and parses result for AuthData +func ParseAuthDataBase64(content []byte) (*AuthData, error) { + + decodedBytes, err := base64.RawStdEncoding.DecodeString(string(content)) + if err != nil && len(decodedBytes) == 0 { + return nil, fmt.Errorf("base64 decoding error: %w", err) + } + return ParseAuthDataJson(decodedBytes) +} + +// ParseAndValidateBase64 parses base64 content for AuthData and validates it +func ParseAndValidateBase64(content []byte, botToken string) (*AuthData, error) { + authData, err := ParseAuthDataBase64(content) + if err != nil { + return nil, err + } + err = authData.Validate([]byte(botToken)) + return authData, err +} + +// ParseAndValidateJson parses json content for AuthData and validates it +func ParseAndValidateJson(content []byte, botToken []byte) (*AuthData, error) { + authData, err := ParseAuthDataJson(content) + if err != nil { + return nil, err + } + err = authData.Validate(botToken) + return authData, err +} + +// GenerateTelegramOAuthURL generates a URL for Telegram OAuth +func GenerateTelegramOAuthURL(botToken, embed, redirect string) string { + bot := strings.Split(botToken, ":") + uri := "https://oauth.telegram.org/auth?bot_id=%s&origin=%s&embed=%s&request_access=write&return_to=%s" + parsedURL, err := url.Parse(redirect) + if err != nil { + return "" + } + return fmt.Sprintf(uri, bot[0], fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host), embed, redirect) +} diff --git a/pkg/oauth/telegram/computations.go b/pkg/oauth/telegram/computations.go new file mode 100644 index 0000000..5ba7867 --- /dev/null +++ b/pkg/oauth/telegram/computations.go @@ -0,0 +1,42 @@ +package telegram + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "fmt" + "reflect" + "sort" + "strings" +) + +// getAuthDataCheckString returns a string ready to calculate validation hash. +// Ref: https://core.telegram.org/widgets/login#checking-authorization +func getAuthDataCheckString(data *AuthData) string { + t := reflect.TypeOf(data).Elem() + v := reflect.ValueOf(data).Elem() + fields := make([]string, 0) + for i := 0; i < t.NumField(); i++ { + tag, _, _ := strings.Cut(t.Field(i).Tag.Get("json"), ",") + if v.Field(i).IsNil() || tag == "hash" { + continue + } + val := v.Field(i).Elem().Interface() + if val == nil || val == "null" { + continue + } + fields = append(fields, fmt.Sprintf("%s=%v", tag, val)) + } + sort.Strings(fields) + return strings.Join(fields, "\n") +} + +// computeHash returns a hash calculated for AuthData +// Ref: https://core.telegram.org/widgets/login#checking-authorization +func computeHash(data *AuthData, botToken []byte) string { + checkString := getAuthDataCheckString(data) + key := sha256.Sum256(botToken) + h := hmac.New(sha256.New, key[:]) + h.Write([]byte(checkString)) + return hex.EncodeToString(h.Sum(nil)) +} diff --git a/pkg/oauth/telegram/model.go b/pkg/oauth/telegram/model.go new file mode 100644 index 0000000..c50529a --- /dev/null +++ b/pkg/oauth/telegram/model.go @@ -0,0 +1,30 @@ +package telegram + +import "fmt" + +type AuthData struct { + Id *int64 `json:"id,omitempty"` + FirstName *string `json:"first_name,omitempty"` + LastName *string `json:"last_name,omitempty"` + Username *string `json:"username,omitempty"` + PhotoUrl *string `json:"photo_url,omitempty"` + AuthDate *int64 `json:"auth_date,omitempty"` + Hash *string `json:"hash,omitempty"` +} + +// Validate checks the hash of AuthData with computed one. To compute hash botToken is required. +// Ref: https://core.telegram.org/widgets/login#checking-authorization +func (d *AuthData) Validate(botToken []byte) error { + if d.Hash == nil { + return fmt.Errorf("auth data has no 'hash' value") + } + if len(botToken) == 0 { + return fmt.Errorf("telegram bot token is not provided") + } + hash := *d.Hash + computedHash := computeHash(d, botToken) + if hash != computedHash { + return fmt.Errorf("hash is not valid") + } + return nil +} diff --git a/pkg/oauth/telegram/telegram.html b/pkg/oauth/telegram/telegram.html new file mode 100644 index 0000000..d868f51 --- /dev/null +++ b/pkg/oauth/telegram/telegram.html @@ -0,0 +1,13 @@ + + + + + + Telegram OAuth Test + + +

Telegram OAuth Test

+ + + diff --git a/pkg/oauth/telegram/telegram_test.go b/pkg/oauth/telegram/telegram_test.go new file mode 100644 index 0000000..2801e70 --- /dev/null +++ b/pkg/oauth/telegram/telegram_test.go @@ -0,0 +1,36 @@ +package telegram + +import ( + "net/http" + "testing" + + "github.com/gin-gonic/gin" +) + +func TestOAuth(t *testing.T) { + t.Skipf("Skip TestOAuth test") + router := gin.Default() + router.LoadHTMLGlob("./*") + router.GET("/telegram", func(c *gin.Context) { + c.HTML(http.StatusOK, "telegram.html", gin.H{ + "title": "Gin HTML Example", + "message": "Hello, Gin!", + }) + }) + router.GET("/auth/telegram/callback", func(c *gin.Context) { + + }) + _ = router.RunTLS(":443", "server.crt", "server.key") +} + +func TestBase64(t *testing.T) { + text := "eyJpZCI6ODI0NjI2ODAzLCJmaXJzdF9uYW1lIjoiQ2hhbmcgbHVlIiwibGFzdF9uYW1lIjoiVHNlbiIsInVzZXJuYW1lIjoidGVuc2lvbl9jIiwicGhvdG9fdXJsIjoiaHR0cHM6XC9cL3QubWVcL2lcL3VzZXJwaWNcLzMyMFwvYU1LNkhEc0pqc2V1YldRYmt2NGlYOHZCRUF6N0hWU3g3dkFuRDBLZ0tFVS5qcGciLCJhdXRoX2RhdGUiOjE3Mzc4MTkwNzQsImhhc2giOiI5M2I1ZDg3Zjc3NjE2YjBjMTM0OTAxYmYwMDg3MTc4YjJiYmZlYzA1MTlkMWVmMDJhZjFjMGNlOTAzM2ZiNGFlIn0" + var token = "7651491571:AAEVQma6niHhtqEYDowAEpPo6Fq69BWvRU8" + + data, err := ParseAndValidateBase64([]byte(text), token) + if err != nil { + t.Error(err) + } + t.Log(*data.Id) + +} diff --git a/pkg/orm/config.go b/pkg/orm/config.go new file mode 100644 index 0000000..447cfd4 --- /dev/null +++ b/pkg/orm/config.go @@ -0,0 +1,22 @@ +package orm + +import ( + "github.com/go-sql-driver/mysql" +) + +func ParseDSN(dsn string) *Config { + cfg, err := mysql.ParseDSN(dsn) + if err != nil { + return nil + } + return &Config{ + Addr: cfg.Addr, + Dbname: cfg.DBName, + Username: cfg.User, + Password: cfg.Passwd, + Config: "charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai", + MaxIdleConns: 10, + MaxOpenConns: 10, + SlowThreshold: 1000, + } +} diff --git a/pkg/orm/mysql.go b/pkg/orm/mysql.go new file mode 100644 index 0000000..8f7584f --- /dev/null +++ b/pkg/orm/mysql.go @@ -0,0 +1,72 @@ +package orm + +import ( + "errors" + "fmt" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/schema" +) + +type Config struct { + Addr string `yaml:"Addr"` + Username string `yaml:"Username"` + Password string `yaml:"Password"` + Dbname string `yaml:"Dbname"` + Config string `yaml:"Config" default:"charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai"` + MaxIdleConns int `yaml:"MaxIdleConns" default:"10"` + MaxOpenConns int `yaml:"MaxOpenConns" default:"10"` + SlowThreshold int64 `yaml:"SlowThreshold" default:"1000"` +} + +type Mysql struct { + Config Config +} + +func (m *Mysql) Dsn() string { + return m.Config.Username + ":" + m.Config.Password + "@tcp(" + m.Config.Addr + ")/" + m.Config.Dbname + "?" + m.Config.Config +} + +func (m *Mysql) GetSlowThreshold() time.Duration { + return time.Duration(m.Config.SlowThreshold) * time.Millisecond +} +func (m *Mysql) GetColorful() bool { + return true +} + +func ConnectMysql(m Mysql) (*gorm.DB, error) { + if m.Config.Dbname == "" { + return nil, errors.New("database name is empty") + } + mysqlCfg := mysql.Config{ + DSN: m.Dsn(), + } + db, err := gorm.Open(mysql.New(mysqlCfg), &gorm.Config{ + Logger: new(logger.GormLogger), + NamingStrategy: schema.NamingStrategy{ + SingularTable: true, + }, + }) + if err != nil { + return nil, err + } else { + sqldb, _ := db.DB() + sqldb.SetMaxIdleConns(m.Config.MaxIdleConns) + sqldb.SetMaxOpenConns(m.Config.MaxOpenConns) + return db, nil + } +} + +func Ping(dsn string) bool { + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) + if err != nil { + fmt.Printf("connect mysql failed, err: %v\n", err.Error()) + return false + } + sqlDB, _ := db.DB() + return sqlDB.Ping() == nil +} diff --git a/pkg/orm/tool_test.go b/pkg/orm/tool_test.go new file mode 100644 index 0000000..6bdb5cd --- /dev/null +++ b/pkg/orm/tool_test.go @@ -0,0 +1,18 @@ +package orm + +import "testing" + +func TestParseDSN(t *testing.T) { + dsn := "root:mylove520@tcp(localhost:3306)/vpnboard" + config := ParseDSN(dsn) + if config == nil { + t.Fatal("config is nil") + } + t.Log(config) +} + +func TestPing(t *testing.T) { + dsn := "root:mylove520@tcp(localhost:3306)/vpnboard" + status := Ping(dsn) + t.Log(status) +} diff --git a/pkg/payment/alipay/alipay.go b/pkg/payment/alipay/alipay.go new file mode 100644 index 0000000..ad6c40e --- /dev/null +++ b/pkg/payment/alipay/alipay.go @@ -0,0 +1,114 @@ +package alipay + +import ( + "context" + "net/url" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/pkg/errors" + "github.com/smartwalle/alipay/v3" +) + +type Config struct { + AppId string + PrivateKey string + PublicKey string + InvoiceName string + NotifyURL string + Sandbox bool +} + +type Notification struct { + OrderNo string + Amount int64 + Status Status +} + +type Status string + +const ( + Success Status = "TRADE_SUCCESS" + Pending Status = "WAIT_BUYER_PAY" + Closed Status = "TRADE_CLOSED" + Finished Status = "TRADE_FINISHED" + Error Status = "TRADE_ERROR" +) + +type Client struct { + Config + client *alipay.Client +} +type Order struct { + OrderNo string + Amount int64 +} + +func NewClient(c Config) *Client { + client, err := alipay.New(c.AppId, c.PrivateKey, c.Sandbox) + if err != nil { + logger.Error("[Alipay] NewClient failed: ", logger.Field("errors", err), logger.Field("config", c)) + return nil + } + err = client.LoadAliPayPublicKey(c.PublicKey) + if err != nil { + logger.Error("[Alipay] NewClient failed: ", logger.Field("errors", err), logger.Field("config", c)) + } + return &Client{ + Config: c, + client: client, + } +} + +func (c *Client) PreCreateTrade(ctx context.Context, order Order) (string, error) { + amountString := tool.FormatFloat(float64(order.Amount)/float64(100), 2) + trade, err := c.client.TradePreCreate(ctx, alipay.TradePreCreate{ + Trade: alipay.Trade{ + OutTradeNo: order.OrderNo, + TotalAmount: amountString, + Subject: c.InvoiceName, + NotifyURL: c.NotifyURL, + }, + }) + if err != nil { + return "", err + } + if trade.Code != alipay.CodeSuccess { + return "", errors.New("PreCreateTrade failed: " + trade.Msg) + } + return trade.QRCode, nil +} + +func (c *Client) QueryTrade(ctx context.Context, orderNo string) (Status, error) { + trade, err := c.client.TradeQuery(ctx, alipay.TradeQuery{ + OutTradeNo: orderNo, + }) + if err != nil { + return Error, err + } + switch trade.TradeStatus { + case alipay.TradeStatusSuccess: + return Success, nil + case alipay.TradeStatusWaitBuyerPay: + return Pending, nil + case alipay.TradeStatusClosed: + return Closed, nil + case alipay.TradeStatusFinished: + return Finished, nil + default: + return Error, errors.New("QueryTrade failed: " + trade.Msg) + } +} + +func (c *Client) DecodeNotification(form url.Values) (*Notification, error) { + notify, err := c.client.DecodeNotification(form) + if err != nil { + return nil, err + } + + return &Notification{ + OrderNo: notify.OutTradeNo, + Amount: int64(tool.FormatStringToFloat(notify.TotalAmount) * 100), + Status: Status(notify.TradeStatus), + }, nil +} diff --git a/pkg/payment/alipay/alipay_test.go b/pkg/payment/alipay/alipay_test.go new file mode 100644 index 0000000..9635e52 --- /dev/null +++ b/pkg/payment/alipay/alipay_test.go @@ -0,0 +1,25 @@ +package alipay + +import ( + "context" + "testing" +) + +func TestClientPreCreateTrade(t *testing.T) { + t.Skipf("Skip TestClientPreCreateTrade") + cfg := Config{ + InvoiceName: "XrayR", + NotifyURL: "https://example.com/alipay/notify", + Sandbox: true, + } + c := NewClient(cfg) + order := Order{ + OrderNo: "20210701000001", + Amount: 100, + } + qr, err := c.PreCreateTrade(context.Background(), order) + if err != nil { + t.Fatal(err) + } + t.Log(qr) +} diff --git a/pkg/payment/epay/epay.go b/pkg/payment/epay/epay.go new file mode 100644 index 0000000..1a7e110 --- /dev/null +++ b/pkg/payment/epay/epay.go @@ -0,0 +1,122 @@ +package epay + +import ( + "encoding/json" + "io" + "net/http" + "net/url" + "sort" + "strings" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/tool" +) + +type Client struct { + Pid string + Url string + Key string +} + +type Order struct { + Name string + OrderNo string + Amount float64 + SignType string + NotifyUrl string + ReturnUrl string +} + +type queryOrderStatusResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + TradeNo string `json:"trade_no"` + OutTradeNo string `json:"out_trade_no"` + Type string `json:"type"` + Status int `json:"status"` +} + +func NewClient(pid, url, key string) *Client { + return &Client{ + Pid: pid, + Url: url, + Key: key, + } +} + +func (c *Client) CreatePayUrl(order Order) string { + // Prepare URL values + params := url.Values{} + params.Set("name", order.Name) + params.Set("money", tool.FormatFloat(order.Amount, 2)) + params.Set("notify_url", order.NotifyUrl) + params.Set("out_trade_no", order.OrderNo) + params.Set("pid", c.Pid) + params.Set("return_url", order.ReturnUrl) + + // Generate the sign using the CreateSign function + sign := c.createSign(c.structToMap(order)) + params.Set("sign", sign) + + // Add sign_type manually + params.Set("sign_type", "MD5") + return c.Url + "/submit.php?" + params.Encode() +} + +func (c *Client) createSign(params map[string]string) string { + keys := make([]string, 0, len(params)) + for k := range params { + if params[k] != "" && k != "sign" && k != "sign_type" { + keys = append(keys, k) + } + } + sort.Strings(keys) + var parts []string + for _, k := range keys { + parts = append(parts, k+"="+params[k]) + } + queryString := strings.Join(parts, "&") + text := queryString + c.Key + return tool.Md5Encode(text, false) +} + +func (c *Client) VerifySign(params map[string]string) bool { + return c.createSign(params) == params["sign"] +} + +func (c *Client) QueryOrderStatus(orderNo string) bool { + client := http.Client{ + Timeout: 5 * time.Second, + } + resp, err := client.Get(c.Url + "/api.php" + "?act=order" + "&pid=" + c.Pid + "&key=" + c.Key + "&out_trade_no=" + orderNo) + if err != nil { + logger.Error("[Epay] QueryOrderStatus error", logger.Field("orderNo", orderNo), logger.Field("error", err.Error())) + return false + } + defer resp.Body.Close() + value, err := io.ReadAll(resp.Body) + if err != nil { + logger.Error("[Epay] QueryOrderStatus error", logger.Field("orderNo", orderNo), logger.Field("error", err.Error())) + return false + } + var response queryOrderStatusResponse + err = json.Unmarshal(value, &response) + if err != nil { + logger.Error("[Epay] QueryOrderStatus error", logger.Field("orderNo", orderNo), logger.Field("error", err.Error())) + return false + } + return response.Status == 1 +} + +// StructToMap converts a struct to map[string]string +func (c *Client) structToMap(order Order) map[string]string { + result := make(map[string]string) + result["money"] = tool.FormatFloat(order.Amount, 2) + result["name"] = order.Name + result["notify_url"] = order.NotifyUrl + result["out_trade_no"] = order.OrderNo + result["pid"] = c.Pid + result["return_url"] = order.ReturnUrl + return result +} diff --git a/pkg/payment/epay/epay_test.go b/pkg/payment/epay/epay_test.go new file mode 100644 index 0000000..a3c6884 --- /dev/null +++ b/pkg/payment/epay/epay_test.go @@ -0,0 +1,49 @@ +package epay + +import "testing" + +func TestEpay(t *testing.T) { + client := NewClient("", "http://127.0.0.1", "") + order := Order{ + Name: "测试", + OrderNo: "123456789", + Amount: 1000, + SignType: "md5", + NotifyUrl: "http://127.0.0.1", + ReturnUrl: "http://127.0.0.1", + } + url := client.CreatePayUrl(order) + t.Logf("PayUrl: %s\n", url) + +} + +func TestQueryOrderStatus(t *testing.T) { + t.Skipf("Skip TestQueryOrderStatus test") + client := NewClient("Pid", "Url", "Key") + orderNo := "123456789" + status := client.QueryOrderStatus(orderNo) + t.Logf("OrderNo: %s, Status: %v\n", orderNo, status) +} + +func TestVerifySign(t *testing.T) { + t.Skipf("Skip TestVerifySign test") + params := map[string]string{ + "pid": "1654", + "trade_no": "2024121521150860990", + "out_trade_no": "202412152115078262977262254", + "type": "alipay", + "name": "product", + "money": "10", + "trade_status": "TRADE_SUCCESS", + "sign": "d3181f18ebdf9821f0ab6ee93faa82d1", + "sign_type": "MD5", + } + + key := "LbTabbB580zWyhXhyyww7wwvy5u8k0wl" + c := NewClient("Pid", "Url", key) + if c.VerifySign(params) { + t.Logf("Sign verification success!") + } else { + t.Error("Sign verification failed!") + } +} diff --git a/pkg/payment/http_test.go b/pkg/payment/http_test.go new file mode 100644 index 0000000..d4ad384 --- /dev/null +++ b/pkg/payment/http_test.go @@ -0,0 +1,21 @@ +package payment + +import ( + "net/http" + "testing" + + "github.com/gin-gonic/gin" +) + +func TestHttp(t *testing.T) { + t.Skipf("Skip TestHttp test") + router := gin.Default() + router.LoadHTMLGlob("./*") + router.GET("/stripe", func(c *gin.Context) { + c.HTML(http.StatusOK, "stripe.html", gin.H{ + "title": "Gin HTML Example", + "message": "Hello, Gin!", + }) + }) + _ = router.Run(":8989") +} diff --git a/pkg/payment/payssion/payssion.go b/pkg/payment/payssion/payssion.go new file mode 100644 index 0000000..fa30293 --- /dev/null +++ b/pkg/payment/payssion/payssion.go @@ -0,0 +1,127 @@ +package payssion + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/perfect-panel/ppanel-server/pkg/md5" + "github.com/pkg/errors" + "go.uber.org/zap" + "io" + "log" + "net/http" +) + +type Order struct { + Name string + OrderNo string + Amount float64 + NotifyUrl string + ReturnUrl string +} + +type Client struct { + Name string + ApiKey string + SecretKey string + QueryUrl string + CreateUrl string + PmId string + Currency string +} + +func NewClient(apiKey string, secretKey, pmId, currency, queryUrl, createUrl string) *Client { + return &Client{ + ApiKey: apiKey, + SecretKey: secretKey, + PmId: pmId, + Currency: currency, + QueryUrl: queryUrl, + CreateUrl: createUrl, + } +} + +func (c *Client) CreateOrder(order Order) (string, error) { + content := fmt.Sprintf("%s|%s|%.2f|%s|%s|%s", c.ApiKey, c.PmId, order.Amount, "USD", order.OrderNo, c.SecretKey) + sign := md5.Sign(content) + params := map[string]string{ + "api_key": c.ApiKey, + "pm_id": c.PmId, + "amount": fmt.Sprintf("%.2f", order.Amount), + "currency": "USD", + "description": "shop", + "order_id": order.OrderNo, + "api_sig": sign, + "return_url": order.ReturnUrl, + } + marshal, _ := json.Marshal(params) + resp, err := http.Post(c.CreateUrl, "application/json", bytes.NewBuffer(marshal)) + if err != nil { + return "", err + } + all, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + log.Println(string(all)) + result := make(map[string]interface{}) + err = json.Unmarshal(all, &result) + if err != nil { + return "", err + } + result_code := result["result_code"] + if fmt.Sprintf("%v", result_code) != "200" { + return "", errors.New(result["description"].(string)) + } + url := result["redirect_url"] + if url == nil { + return "", errors.New(string(all)) + } + return url.(string), nil +} + +func (c *Client) QueryOrder(orderNo string) (queryResult *QueryResult, err error) { + content := fmt.Sprintf("%s|%s|%s", c.ApiKey, orderNo, c.SecretKey) + sign := md5.Sign(content) + params := map[string]string{ + "api_key": c.ApiKey, + "order_id": orderNo, + "api_sig": sign, + } + marshal, _ := json.Marshal(params) + resp, err := http.Post(c.QueryUrl, "application/json", bytes.NewBuffer(marshal)) + if err != nil { + return nil, err + } + all, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + zap.S().Infof("Payssion QueryOrderDetail result: %s", string(all)) + err = json.Unmarshal(all, &queryResult) + return queryResult, err +} + +type QueryResult struct { + Transaction struct { + TransactionID string `json:"transaction_id"` + Description string `json:"description"` + AppName string `json:"app_name"` + PmID string `json:"pm_id"` + Amount string `json:"amount"` + Currency string `json:"currency"` + OrderID string `json:"order_id"` + Paid string `json:"paid"` + Net string `json:"net"` + State string `json:"state"` + Fee string `json:"fee"` + Refund string `json:"refund"` + RefundFee string `json:"refund_fee"` + Created int64 `json:"created"` + Updated int64 `json:"updated"` + Fees string `json:"fees"` + FeesAdd string `json:"fees_add"` + RefundFees string `json:"refund_fees"` + } `json:"transaction"` + ResultCode int64 `json:"result_code"` +} diff --git a/pkg/payment/payssion/payssion_test.go b/pkg/payment/payssion/payssion_test.go new file mode 100644 index 0000000..8f5ae34 --- /dev/null +++ b/pkg/payment/payssion/payssion_test.go @@ -0,0 +1,32 @@ +package payssion + +import ( + "fmt" + "testing" +) + +func TestCreateOrder(t *testing.T) { + client := Client{ + ApiKey: "", + PmId: "", + SecretKey: "", + QueryUrl: "http://sandbox.payssion.com/api/v1/payment/getDetail", + CreateUrl: "http://sandbox.payssion.com/api/v1/payments", + } + order := Order{ + Name: "shop", + OrderNo: "123", + Amount: 1000, + } + createOrder, err := client.CreateOrder(order) + if err != nil { + t.Error(err) + return + } + fmt.Println(createOrder) + queryOrder, err := client.QueryOrder(order.OrderNo) + if err != nil { + t.Error(err) + } + fmt.Println(queryOrder.Transaction.State) +} diff --git a/pkg/payment/platform.go b/pkg/payment/platform.go new file mode 100644 index 0000000..b749c83 --- /dev/null +++ b/pkg/payment/platform.go @@ -0,0 +1,86 @@ +package payment + +import "github.com/perfect-panel/ppanel-server/internal/types" + +type Platform int + +const ( + Stripe Platform = iota + AlipayF2F + EPay + Balance + Payssion + UNSUPPORTED +) + +var platformNames = map[string]Platform{ + "Stripe": Stripe, + "AlipayF2F": AlipayF2F, + "EPay": EPay, + "Payssion": Payssion, + "balance": Balance, + "unsupported": UNSUPPORTED, +} + +func (p Platform) String() string { + for k, v := range platformNames { + if v == p { + return k + } + } + return "unsupported" +} + +func ParsePlatform(s string) Platform { + if p, ok := platformNames[s]; ok { + return p + } + return UNSUPPORTED +} + +func GetSupportedPlatforms() []types.PlatformInfo { + return []types.PlatformInfo{ + { + Platform: Stripe.String(), + PlatformUrl: "https://stripe.com", + PlatformFieldDescription: map[string]string{ + "public_key": "Publishable key", + "secret_key": "Secret key", + "webhook_secret": "Webhook secret", + "payment": "Payment Method, only supported card/alipay/wechat_pay", + }, + }, + { + Platform: AlipayF2F.String(), + PlatformUrl: "https://alipay.com", + PlatformFieldDescription: map[string]string{ + "app_id": "App ID", + "private_key": "Private Key", + "public_key": "Public Key", + "invoice_name": "Invoice Name", + "sandbox": "Sandbox Mode", + }, + }, + { + Platform: EPay.String(), + PlatformUrl: "", + PlatformFieldDescription: map[string]string{ + "pid": "PID", + "url": "URL", + "key": "Key", + }, + }, + { + Platform: Payssion.String(), + PlatformUrl: "", + PlatformFieldDescription: map[string]string{ + "api_key": "api_key", + "secret_key": "secret_key", + "pm_id": "pm_id", + "currency": "currency", + "create_url": "Create URL", + "query_url": "Query URL", + }, + }, + } +} diff --git a/pkg/payment/stripe.html b/pkg/payment/stripe.html new file mode 100644 index 0000000..f0ab17b --- /dev/null +++ b/pkg/payment/stripe.html @@ -0,0 +1,40 @@ + + + + + Title + + + + + + + \ No newline at end of file diff --git a/pkg/payment/stripe/stripe.go b/pkg/payment/stripe/stripe.go new file mode 100644 index 0000000..4bd5080 --- /dev/null +++ b/pkg/payment/stripe/stripe.go @@ -0,0 +1,195 @@ +package stripe + +import ( + "encoding/json" + "fmt" + "strconv" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/stripe/stripe-go/v81" + "github.com/stripe/stripe-go/v81/customer" + "github.com/stripe/stripe-go/v81/ephemeralkey" + "github.com/stripe/stripe-go/v81/paymentintent" + "github.com/stripe/stripe-go/v81/paymentmethod" + "github.com/stripe/stripe-go/v81/webhook" +) + +const APIVersion = "2024-04-10" + +type Config struct { + PublicKey string + SecretKey string + WebhookSecret string +} + +type User struct { + UserId int64 + Email string +} +type NotifyResult struct { + EventType string + OrderNo string + TradeNo string + Method string + UserId int64 + Amount int64 +} +type Order struct { + OrderNo string + Subscribe string + Amount int64 + Currency string + Payment string +} + +type Client struct { + Config +} + +type PaymentSheet struct { + ClientSecret string + EphemeralKey string + Customer string + PublishableKey string + TradeNo string +} + +func NewClient(config Config) *Client { + return &Client{ + Config: config, + } +} + +func (c *Client) CreatePaymentSheet(order *Order, user *User) (*PaymentSheet, error) { + stripe.Key = c.SecretKey + // Create a new Stripe customer if it does not exist + customerDataRes, err := c.SearchStripeCustomer(user) + if err != nil { + return nil, err + } + if customerDataRes == nil { + customerDataRes, err = c.CreateCustomer(user) + if err != nil { + return nil, err + } + } + // Create Ephemeral Key + ekParams := &stripe.EphemeralKeyParams{ + Customer: stripe.String(customerDataRes.ID), + StripeVersion: stripe.String(APIVersion), + } + ek, err := ephemeralkey.New(ekParams) + if err != nil { + return nil, err + } + // Create Payment Intent + params := &stripe.PaymentIntentParams{ + Amount: stripe.Int64(order.Amount), + Customer: stripe.String(customerDataRes.ID), + Currency: stripe.String(order.Currency), + PaymentMethodTypes: []*string{ + stripe.String(order.Payment), + }, + Metadata: map[string]string{ + "order_no": order.OrderNo, + "user_id": strconv.FormatInt(user.UserId, 10), + "subscribe": order.Subscribe, + }, + } + result, err := paymentintent.New(params) + if err != nil { + return nil, err + } + return &PaymentSheet{ + ClientSecret: result.ClientSecret, + EphemeralKey: ek.Secret, + Customer: customerDataRes.ID, + PublishableKey: c.PublicKey, + TradeNo: result.ID, + }, nil +} + +// SearchStripeCustomer Search for a Stripe customer by email or user ID +func (c *Client) SearchStripeCustomer(user *User) (*stripe.Customer, error) { + stripe.Key = c.SecretKey + params := &stripe.CustomerSearchParams{} + if user.Email != "" { + params.SearchParams.Query = fmt.Sprintf("email:'%s'", user.Email) + } else { + params.SearchParams.Query = fmt.Sprintf("metadata['user_id']:'%d'", user.UserId) + } + result := customer.Search(params) + if result.Err() != nil { + fmt.Printf("Error: %v\n", result.Err().Error()) + return nil, result.Err() + } + + if len(result.CustomerSearchResult().Data) != 0 { + return result.CustomerSearchResult().Data[0], nil + } + return nil, nil +} + +// CreateCustomer Create a new Stripe customer +func (c *Client) CreateCustomer(user *User) (*stripe.Customer, error) { + stripe.Key = c.SecretKey + customerData := &stripe.CustomerParams{} + if user.Email != "" { + customerData.Email = &user.Email + } + customerData.AddMetadata("user_id", strconv.FormatInt(user.UserId, 10)) + return customer.New(customerData) +} + +// QueryOrderStatus Query the status of the order +func (c *Client) QueryOrderStatus(orderNo string) (bool, error) { + stripe.Key = c.SecretKey + intent, err := paymentintent.Get(orderNo, nil) + if err != nil { + return false, err + } + return intent.Status == "succeeded", err +} + +// ParseNotify +func (c *Client) ParseNotify(payload []byte, signature string) (*NotifyResult, error) { + event, err := webhook.ConstructEvent(payload, signature, c.Config.WebhookSecret) + if err != nil { + return nil, err + } + var paymentIntent stripe.PaymentIntent + err = json.Unmarshal(event.Data.Raw, &paymentIntent) + if err != nil { + logger.Error("Failed to unmarshal payment intent", logger.Field("error", err.Error())) + return nil, err + } + orderNo := paymentIntent.Metadata["order_no"] + userId := paymentIntent.Metadata["user_id"] + var method string + if paymentIntent.PaymentMethod != nil && paymentIntent.PaymentMethod.ID != "" { + fmt.Println("paymentMethod:", paymentIntent.PaymentMethod.ID) + result, err := c.RetrievePaymentMethod(paymentIntent.PaymentMethod.ID) + if err != nil { + logger.Error("[stripe] Payment callback query payment method error", logger.Field("errors", err.Error())) + } + if result != nil { + method = string(result.Type) + } + } + // userId string 转 int64 + uid, _ := strconv.ParseInt(userId, 10, 64) + return &NotifyResult{ + EventType: string(event.Type), + OrderNo: orderNo, + TradeNo: paymentIntent.ID, + UserId: uid, + Method: method, + Amount: paymentIntent.Amount, + }, nil +} + +// RetrievePaymentMethod 查询支付方式 +func (c *Client) RetrievePaymentMethod(id string) (*stripe.PaymentMethod, error) { + stripe.Key = c.SecretKey + return paymentmethod.Get(id, nil) +} diff --git a/pkg/payment/stripe/stripe_test.go b/pkg/payment/stripe/stripe_test.go new file mode 100644 index 0000000..872b9e9 --- /dev/null +++ b/pkg/payment/stripe/stripe_test.go @@ -0,0 +1,55 @@ +package stripe + +import ( + "testing" + + "github.com/stripe/stripe-go/v81" +) + +func TestStripeAlipay(t *testing.T) { + t.Skipf("Skip TestStripeAlipay test") + client := NewClient(Config{ + WebhookSecret: "", + }) + order := Order{ + OrderNo: "JS20210719123456789", + Subscribe: "测试", + Amount: 100, + Currency: string(stripe.CurrencyGBP), + Payment: "alipay", + } + user := User{ + UserId: 1, + Email: "tension@muran.org", + } + result, err := client.CreatePaymentSheet(&order, &user) + if err != nil { + t.Error(err.Error()) + } + t.Logf("TradeNo: %s\n", result.ClientSecret) +} + +func TestStripeWechat(t *testing.T) { + t.Skipf("Skip TestStripeWechat test") + client := NewClient(Config{ + SecretKey: "SecretKey", + PublicKey: "PublicKey", + WebhookSecret: "", + }) + order := Order{ + OrderNo: "JS20210719123456789", + Subscribe: "测试", + Amount: 100, + Currency: string(stripe.CurrencyGBP), + Payment: "wechat_pay", + } + user := User{ + UserId: 1, + Email: "tension@muran.org", + } + result, err := client.CreatePaymentSheet(&order, &user) + if err != nil { + t.Error(err.Error()) + } + t.Logf("TradeNo: %s\n", result.ClientSecret) +} diff --git a/pkg/phone/phone.go b/pkg/phone/phone.go new file mode 100644 index 0000000..022b754 --- /dev/null +++ b/pkg/phone/phone.go @@ -0,0 +1,113 @@ +package phone + +import ( + "fmt" + "regexp" + + "github.com/nyaruka/phonenumbers" +) + +func Check(areaCode, telephone string) bool { + parsedNumber, err := phonenumbers.Parse(fmt.Sprintf("+%s%s", areaCode, telephone), areaCode) + if err != nil { + return false + } + // 检查手机号是否有效 + return phonenumbers.IsValidNumber(parsedNumber) +} + +func CheckPhone(telephone string) bool { + parsedNumber, err := phonenumbers.Parse(fmt.Sprintf("+%s", telephone), "") + if err != nil { + return false + } + // 检查手机号是否有效 + return phonenumbers.IsValidNumber(parsedNumber) +} + +func GetCountryCode(telephone string) string { + parsedNumber, err := phonenumbers.Parse(fmt.Sprintf("+%s", telephone), "") + if err != nil { + return "" + } + return fmt.Sprintf("%d", *parsedNumber.CountryCode) +} + +func FormatToInternational(telephone string) string { + parsedNumber, err := phonenumbers.Parse(fmt.Sprintf("+%s", telephone), "") + if err != nil { + return "" + } + return phonenumbers.Format(parsedNumber, phonenumbers.INTERNATIONAL) +} + +func FormatToE164(area, phone string) (string, error) { + parsedNumber, err := phonenumbers.Parse(fmt.Sprintf("+%s%s", area, phone), "") + if err != nil { + return "", err + } + return phonenumbers.Format(parsedNumber, phonenumbers.E164), nil +} + +// MaskPhoneNumber 解析并脱敏电话号码 +func MaskPhoneNumber(phone string) string { + // 解析电话号码 + num, err := phonenumbers.Parse(phone, "") + if err != nil { + return "" + } + // 获取国际格式,如 "+1 512-345-6789" + formatted := phonenumbers.Format(num, phonenumbers.INTERNATIONAL) + + // 使用正则匹配国家代码和号码部分 + re := regexp.MustCompile(`(\+\d{1,3})\s*(.*)`) + matches := re.FindStringSubmatch(formatted) + if len(matches) < 3 { + return formatted // 如果格式不匹配,返回原格式 + } + + countryCode := matches[1] // 国家代码(如 "+1") + numberPart := matches[2] // 本地号码部分(如 "512-345-6789") + + // 根据不同国家的号码格式进行脱敏 + maskedNumber := maskDigits(numberPart, countryCode) + + // 组合脱敏后的号码 + return fmt.Sprintf("%s %s", countryCode, maskedNumber) +} + +// maskDigits 替换部分数字,保持位数和分隔符 +func maskDigits(number string, countryCode string) string { + // 统计数字个数 + digitCount := 0 + for _, r := range number { + if r >= '0' && r <= '9' { + digitCount++ + } + } + + // 处理不同国家的号码格式 + runes := []rune(number) + digitIndex := 0 + + for i, r := range runes { + if r >= '0' && r <= '9' { + digitIndex++ + if countryCode == "+1" { // 美国号码格式:+1 (512) ***-1278 + if digitIndex > 3 && digitIndex <= digitCount-4 { // 只替换中间部分 + runes[i] = '*' + } + } else if countryCode == "+86" { // 中国号码格式:+86 138 **** 5678 + if digitIndex > 3 && digitIndex <= digitCount-4 { + runes[i] = '*' + } + } else { // 其他国家号码,采用类似规则 + if digitIndex > 3 && digitIndex <= digitCount-4 { + runes[i] = '*' + } + } + } + } + + return string(runes) +} diff --git a/pkg/phone/phone_test.go b/pkg/phone/phone_test.go new file mode 100644 index 0000000..23414e3 --- /dev/null +++ b/pkg/phone/phone_test.go @@ -0,0 +1,57 @@ +package phone + +import ( + "testing" + + "github.com/nyaruka/phonenumbers" +) + +func TestPhoneNumber(t *testing.T) { + parsedNumber, err := phonenumbers.Parse("+8615502505555", "") + if err != nil { + t.Fatalf("Failed to parse phone number: %v", err) + return + } + // 检查手机号是否有效 + isValid := phonenumbers.IsValidNumber(parsedNumber) + // 获取区域代码 (如 CN, US, IN) + region := phonenumbers.GetRegionCodeForNumber(parsedNumber) + t.Log(isValid) + t.Log(region) +} + +func TestCheck(t *testing.T) { + var phone = "15502505555" + if !Check("86", phone) { + t.Fatalf("Check phone number failed: %s", phone) + } + t.Logf("Check phone number success: %s", phone) +} + +func TestGetCountryCode(t *testing.T) { + var phone = "14407941888" + countryCode := GetCountryCode(phone) + t.Logf("Country code: %s", countryCode) +} + +func TestFormatToInternational(t *testing.T) { + var phone = "8615502505555" + international := FormatToInternational(phone) + t.Logf("International format: %s", international) +} + +func TestFormatToE164(t *testing.T) { + var phone = "4407941888" + e164, err := FormatToE164("1", phone) + if err != nil { + t.Fatalf("Failed to format phone number to E164: %v", err) + return + } + t.Logf("E164 format: %s", e164) +} + +func TestMask(t *testing.T) { + var phone = "+14407941888" + mask := MaskPhoneNumber(phone) + t.Logf("Mask format: %s", mask) +} diff --git a/pkg/proc/shutdown+polyfill.go b/pkg/proc/shutdown+polyfill.go new file mode 100644 index 0000000..f371f57 --- /dev/null +++ b/pkg/proc/shutdown+polyfill.go @@ -0,0 +1,34 @@ +//go:build windows + +package proc + +import "time" + +// ShutdownConf is empty on windows. +type ShutdownConf struct{} + +// AddShutdownListener returns fn itself on windows, lets callers call fn on their own. +func AddShutdownListener(fn func()) func() { + return fn +} + +// AddWrapUpListener returns fn itself on windows, lets callers call fn on their own. +func AddWrapUpListener(fn func()) func() { + return fn +} + +// SetTimeToForceQuit does nothing on windows. +func SetTimeToForceQuit(duration time.Duration) { +} + +// Setup does nothing on windows. +func Setup(conf ShutdownConf) { +} + +// Shutdown does nothing on windows. +func Shutdown() { +} + +// WrapUp does nothing on windows. +func WrapUp() { +} diff --git a/pkg/proc/shutdown.go b/pkg/proc/shutdown.go new file mode 100644 index 0000000..d08b466 --- /dev/null +++ b/pkg/proc/shutdown.go @@ -0,0 +1,100 @@ +//go:build linux || darwin + +package proc + +import ( + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/threading" +) + +const ( + //nolint:unused + wrapUpTime = time.Second + // why we use 5500 milliseconds is because most of our queue are blocking mode with 5 seconds + waitTime = 5500 * time.Millisecond +) + +var ( + wrapUpListeners = new(listenerManager) + shutdownListeners = new(listenerManager) + delayTimeBeforeForceQuit = waitTime +) + +// AddShutdownListener adds fn as a shutdown listener. +// The returned func can be used to wait for fn getting called. +func AddShutdownListener(fn func()) (waitForCalled func()) { + return shutdownListeners.addListener(fn) +} + +// AddWrapUpListener adds fn as a wrap up listener. +// The returned func can be used to wait for fn getting called. +func AddWrapUpListener(fn func()) (waitForCalled func()) { + return wrapUpListeners.addListener(fn) +} + +// SetTimeToForceQuit sets the waiting time before force quitting. +func SetTimeToForceQuit(duration time.Duration) { + delayTimeBeforeForceQuit = duration +} + +// Shutdown calls the registered shutdown listeners, only for test purpose. +func Shutdown() { + shutdownListeners.notifyListeners() +} + +// WrapUp wraps up the process, only for test purpose. +func WrapUp() { + wrapUpListeners.notifyListeners() +} + +//nolint:unused +func gracefulStop(signals chan os.Signal, sig syscall.Signal) { + signal.Stop(signals) + + go wrapUpListeners.notifyListeners() + + time.Sleep(wrapUpTime) + go shutdownListeners.notifyListeners() + + time.Sleep(delayTimeBeforeForceQuit - wrapUpTime) + _ = syscall.Kill(syscall.Getpid(), sig) +} + +type listenerManager struct { + lock sync.Mutex + waitGroup sync.WaitGroup + listeners []func() +} + +func (lm *listenerManager) addListener(fn func()) (waitForCalled func()) { + lm.waitGroup.Add(1) + + lm.lock.Lock() + lm.listeners = append(lm.listeners, func() { + defer lm.waitGroup.Done() + fn() + }) + lm.lock.Unlock() + + return func() { + lm.waitGroup.Wait() + } +} + +func (lm *listenerManager) notifyListeners() { + lm.lock.Lock() + defer lm.lock.Unlock() + + group := threading.NewRoutineGroup() + for _, listener := range lm.listeners { + group.RunSafe(listener) + } + group.Wait() + + lm.listeners = nil +} diff --git a/pkg/proc/shutdown_test.go b/pkg/proc/shutdown_test.go new file mode 100644 index 0000000..efd5104 --- /dev/null +++ b/pkg/proc/shutdown_test.go @@ -0,0 +1,62 @@ +//go:build linux || darwin + +package proc + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestShutdown(t *testing.T) { + SetTimeToForceQuit(time.Hour) + assert.Equal(t, time.Hour, delayTimeBeforeForceQuit) + + var val int + called := AddWrapUpListener(func() { + val++ + }) + WrapUp() + called() + assert.Equal(t, 1, val) + + called = AddShutdownListener(func() { + val += 2 + }) + Shutdown() + called() + assert.Equal(t, 3, val) +} + +func TestNotifyMoreThanOnce(t *testing.T) { + ch := make(chan struct{}, 1) + + go func() { + var val int + called := AddWrapUpListener(func() { + val++ + }) + WrapUp() + WrapUp() + called() + assert.Equal(t, 1, val) + + called = AddShutdownListener(func() { + val += 2 + }) + Shutdown() + Shutdown() + called() + assert.Equal(t, 3, val) + ch <- struct{}{} + }() + + select { + case <-ch: + fmt.Printf("TestNotifyMoreThanOnce done\n") + case <-time.After(time.Second): + t.Fatal("timeout, check error logs") + } +} diff --git a/pkg/random/RandomKey.go b/pkg/random/RandomKey.go new file mode 100644 index 0000000..9b1d0aa --- /dev/null +++ b/pkg/random/RandomKey.go @@ -0,0 +1,113 @@ +package random + +import ( + "strings" + "time" + + "k8s.io/apimachinery/pkg/util/rand" +) + +const ( + chars62 = "E7gLp4jWS6kPv5DzxaY1o9sNcFmBAlUut0ZOhKVM38bqHRJfCwdrTni2QIeXGy" + base62 = int64(len(chars62)) + + chars36 = "6W1HLYPUSJ745ZAKMBQEN9DF8OVGITX320RC" + base36 = int64(len(chars36)) +) + +func EncodeBase62(id int64) string { + if id == 0 { + return string(chars62[0]) + } + + encoded := "" + for id > 0 { + remainder := id % base62 + encoded = string(chars62[remainder]) + encoded + id /= base62 + } + + index := len(chars62) - 1 + for len(encoded) < 6 { + encoded = string(chars62[index]) + encoded + index -= 3 + if index < 0 { + index = len(chars62) - 1 + } + } + // if len(encoded) > 7 { + // encoded = encoded[:7] + // } + + return encoded +} + +// EncodeBase36 ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 +func EncodeBase36(id int64) string { + if id == 0 { + return string(chars36[0]) + } + + encoded := "" + for id > 0 { + remainder := id % base36 + encoded = string(chars36[remainder]) + encoded + id /= base36 + } + + index := len(chars36) - 1 + for len(encoded) < 6 { + encoded = string(chars62[index]) + encoded + index -= 3 + if index < 0 { + index = len(chars62) - 1 + } + } + // if len(encoded) > 7 { + // encoded = encoded[:7] + // } + + return encoded +} + +func Key(length int, keyType int) string { + randomString := "0123456789" + if keyType == 1 { + randomString = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz" + } + var res []byte + rand.Seed(time.Now().UnixNano()) + for i := 0; i < length; i++ { + n := rand.Intn(len(randomString)) + res = append(res, randomString[n]) + } + return string(res) +} + +func KeyNew(length int, keyType int) string { + randomString := "0123456789" + if keyType == 1 { + randomString = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz" + } else if keyType == 2 { + randomString = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + } + var res []byte + for i := 0; i < length; i++ { + n := rand.Intn(len(randomString)) + res = append(res, randomString[n]) + } + return string(res) +} + +func StrToDashedString(strNum string) string { + var result strings.Builder + + for i, ch := range strNum { + result.WriteRune(ch) + if (i+1)%4 == 0 && i != len(strNum)-1 { + result.WriteRune('-') + } + } + + return result.String() +} diff --git a/pkg/random/RandomKey_test.go b/pkg/random/RandomKey_test.go new file mode 100644 index 0000000..a5f23fd --- /dev/null +++ b/pkg/random/RandomKey_test.go @@ -0,0 +1,96 @@ +package random + +import ( + "math/rand" + "testing" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/snowflake" + + "github.com/stretchr/testify/assert" +) + +func TestEncodeBase62(t *testing.T) { + start := 1112275807 + length := 1558080 + n := length + start + // n := 328564998144 + m := make(map[string]struct{}) + // m := make(map[string]struct{}, length) + var inviteCode string + for i := start; i < n; i++ { + // inviteCode = EncodeBase36(int64(i)) + inviteCode = EncodeBase36(snowflake.GetID()) + if v, ok := m[inviteCode]; ok { + t.Fatal(v, inviteCode) + } + m[inviteCode] = struct{}{} + } + t.Log(inviteCode) + + assert.Equal(t, length, len(m)) +} + +func TestInt64ToDashedString(t *testing.T) { + type args struct { + strNum string + } + tests := []struct { + name string + args args + want string + }{ + // TODO: Add test cases. + { + name: "", + args: args{ + strNum: "123", + }, + want: "123", + }, + { + name: "", + args: args{ + strNum: "1234", + }, + want: "1234", + }, + { + name: "", + args: args{ + strNum: "12345", + }, + want: "1234-5", + }, + { + name: "", + args: args{ + strNum: "12345678", + }, + want: "1234-5678", + }, + { + name: "", + args: args{ + strNum: "123456789", + }, + want: "1234-5678-9", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, StrToDashedString(tt.args.strNum), "StrToDashedString(%v)", tt.args.strNum) + }) + } +} + +// ShuffleString shuffles the characters in a string. +func ShuffleString(s string) string { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + runes := []rune(s) + for i := range runes { + j := r.Intn(i + 1) + runes[i], runes[j] = runes[j], runes[i] + } + return string(runes) +} diff --git a/pkg/random/tool.go b/pkg/random/tool.go new file mode 100644 index 0000000..2b10de8 --- /dev/null +++ b/pkg/random/tool.go @@ -0,0 +1,14 @@ +package random + +import ( + "math/rand" + "time" +) + +func RandomInRange(min, max int) int { + if min > max { + min, max = max, min + } + r := rand.New(rand.NewSource(time.Now().UnixNano())) + return r.Intn(max-min+1) + min +} diff --git a/pkg/rescue/recover.go b/pkg/rescue/recover.go new file mode 100644 index 0000000..3138cb6 --- /dev/null +++ b/pkg/rescue/recover.go @@ -0,0 +1,34 @@ +package rescue + +import ( + "context" + "log" + "runtime/debug" + + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +// Recover is used with defer to do cleanup on panics. +// Use it like: +// +// defer Recover(func() {}) +func Recover(cleanups ...func()) { + for _, cleanup := range cleanups { + cleanup() + } + + if p := recover(); p != nil { + log.Print(p) + } +} + +// RecoverCtx is used with defer to do cleanup on panics. +func RecoverCtx(ctx context.Context, cleanups ...func()) { + for _, cleanup := range cleanups { + cleanup() + } + + if p := recover(); p != nil { + logger.WithContext(ctx).Errorf("%+v\n%s", p, debug.Stack()) + } +} diff --git a/pkg/rescue/recover_test.go b/pkg/rescue/recover_test.go new file mode 100644 index 0000000..ae5d636 --- /dev/null +++ b/pkg/rescue/recover_test.go @@ -0,0 +1,40 @@ +package rescue + +import ( + "context" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" +) + +func init() { +} + +func TestRescue(t *testing.T) { + var count int32 + assert.NotPanics(t, func() { + defer Recover(func() { + atomic.AddInt32(&count, 2) + }, func() { + atomic.AddInt32(&count, 3) + }) + + panic("hello") + }) + assert.Equal(t, int32(5), atomic.LoadInt32(&count)) +} + +func TestRescueCtx(t *testing.T) { + var count int32 + assert.NotPanics(t, func() { + defer RecoverCtx(context.Background(), func() { + atomic.AddInt32(&count, 2) + }, func() { + atomic.AddInt32(&count, 3) + }) + + panic("hello") + }) + assert.Equal(t, int32(5), atomic.LoadInt32(&count)) +} diff --git a/pkg/result/httpResult.go b/pkg/result/httpResult.go new file mode 100644 index 0000000..728115a --- /dev/null +++ b/pkg/result/httpResult.go @@ -0,0 +1,40 @@ +package result + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/pkg/errors" + + "github.com/perfect-panel/ppanel-server/pkg/xerr" +) + +// HttpResult HTTP Result +func HttpResult(ctx *gin.Context, resp interface{}, err error) { + + if err == nil { + // Success Result + ctx.JSON(http.StatusOK, Success(resp)) + return + } + + // Init Error Code and Message + code := xerr.ERROR + msg := "Internal Server Error" + + // Get Error Type + var e *xerr.CodeError + if errors.As(errors.Cause(err), &e) { + // Custom Code Error + code = e.GetErrCode() + msg = e.GetErrMsg() + } + ctx.JSON(http.StatusOK, Error(code, msg)) +} + +// ParamErrorResult Param Error Result +func ParamErrorResult(ctx *gin.Context, err error) { + errMsg := err.Error() + _ = ctx.Error(errors.New(errMsg)) + ctx.JSON(http.StatusOK, Error(xerr.InvalidParams, errMsg)) +} diff --git a/pkg/result/responseBean.go b/pkg/result/responseBean.go new file mode 100644 index 0000000..f6ca3ea --- /dev/null +++ b/pkg/result/responseBean.go @@ -0,0 +1,21 @@ +package result + +type ResponseSuccessBean struct { + Code uint32 `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data,omitempty"` +} +type NullJson struct{} + +func Success(data interface{}) *ResponseSuccessBean { + return &ResponseSuccessBean{200, "success", data} +} + +type ResponseErrorBean struct { + Code uint32 `json:"code"` + Msg string `json:"msg"` +} + +func Error(errCode uint32, errMsg string) *ResponseErrorBean { + return &ResponseErrorBean{errCode, errMsg} +} diff --git a/pkg/rules/errors.go b/pkg/rules/errors.go new file mode 100644 index 0000000..91ff3e6 --- /dev/null +++ b/pkg/rules/errors.go @@ -0,0 +1,8 @@ +package rules + +import "errors" + +var ( + ErrRuleTypeNotFound = errors.New("rule type not found") + ErrRuleTargetNotFound = errors.New("rule target not found") +) diff --git a/pkg/rules/model.go b/pkg/rules/model.go new file mode 100644 index 0000000..d35c7b3 --- /dev/null +++ b/pkg/rules/model.go @@ -0,0 +1,44 @@ +package rules + +type RuleType int + +const ( + Domain RuleType = iota + DomainSuffix + DomainKeyword + GEOIP + IPCIDR + SrcIPCIDR + SrcPort + DstPort + InboundPort + Process + ProcessPath + IPSet + MATCH + Unknown +) + +var ruleTypeMap = map[RuleType]string{ + Domain: "DOMAIN", + DomainSuffix: "DOMAIN-SUFFIX", + DomainKeyword: "DOMAIN-KEYWORD", + GEOIP: "GEOIP", + IPCIDR: "IP-CIDR", + SrcIPCIDR: "SRC-IP-CIDR", + SrcPort: "SRC-PORT", + DstPort: "DST-PORT", + InboundPort: "INBOUND-PORT", + Process: "PROCESS-NAME", + ProcessPath: "PROCESS-PATH", + IPSet: "IPSET", + MATCH: "MATCH", + Unknown: "UNKNOWN", +} + +func (rt RuleType) String() string { + if str, ok := ruleTypeMap[rt]; ok { + return str + } + return "UNKNOWN" +} diff --git a/pkg/rules/parse.go b/pkg/rules/parse.go new file mode 100644 index 0000000..520a303 --- /dev/null +++ b/pkg/rules/parse.go @@ -0,0 +1,10 @@ +package rules + +func ParseRuleType(ruleType string) RuleType { + for k, v := range ruleTypeMap { + if v == ruleType { + return k + } + } + return Unknown +} diff --git a/pkg/rules/rules.go b/pkg/rules/rules.go new file mode 100644 index 0000000..b9523d0 --- /dev/null +++ b/pkg/rules/rules.go @@ -0,0 +1,50 @@ +package rules + +import ( + "strings" +) + +const noResolve = "no-resolve" + +type Rule struct { + Type string + Payload string + Target string +} + +func NewRule(text, name string) *Rule { + rule := trimArr(strings.Split(text, ",")) + var ( + payload string + target string + ) + switch l := len(rule); { + case l == 2: + payload = rule[1] + target = name + case l == 3: + payload = rule[1] + target = rule[2] + case l >= 4: + payload = rule[1] + target = rule[2] + default: + return nil + } + rule = trimArr(rule) + return &Rule{ + Type: rule[0], + Payload: payload, + Target: target, + } +} + +func (r *Rule) String() string { + text := r.Type + "," + r.Payload + "," + r.Target + switch ParseRuleType(r.Type) { + case IPCIDR, IPSet: + return text + "," + noResolve + default: + return text + } +} diff --git a/pkg/rules/rules_test.go b/pkg/rules/rules_test.go new file mode 100644 index 0000000..cdb9cad --- /dev/null +++ b/pkg/rules/rules_test.go @@ -0,0 +1,52 @@ +package rules + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +var text = ` +DOMAIN,example.com +DOMAIN-SUFFIX,google.com,DIRECT +DOMAIN-KEYWORD,amazon,REJECT +IP-CIDR,192.168.0.0/16 +` + +func TestNewRule(t *testing.T) { + var rs []string + // parse validate rules + ruleArr := strings.Split(text, "\n") + if len(ruleArr) == 0 { + t.Error("rules is empty") + } + ruleArr = trimArr(ruleArr) + for _, s := range ruleArr { + r := NewRule(s, "Test") + if r == nil { + t.Errorf("[CreateRuleGroup] rule %s is nil, len: %d", s, len(s)) + continue + } + if err := r.Validate(); err != nil { + t.Errorf("[CreateRuleGroup] rule %s is invalid: %v", s, err) + continue + } + rs = append(rs, r.String()) + } + + expected := []string{ + "DOMAIN,example.com,Test", + "DOMAIN-SUFFIX,google.com,DIRECT", + "DOMAIN-KEYWORD,amazon,REJECT", + "IP-CIDR,192.168.0.0/16,Test,no-resolve", + } + + for i, r := range rs { + if r != expected[i] { + t.Errorf("expected %s, got %s", expected[i], r) + } + } + // Check if the rules are sorted + assert.Equal(t, len(rs), len(expected)) +} diff --git a/pkg/rules/utils.go b/pkg/rules/utils.go new file mode 100644 index 0000000..5f13b1c --- /dev/null +++ b/pkg/rules/utils.go @@ -0,0 +1,14 @@ +package rules + +import "strings" + +func trimArr(arr []string) []string { + var result []string + for _, s := range arr { + trimmed := strings.TrimSpace(s) + if trimmed != "" { + result = append(result, trimmed) + } + } + return result +} diff --git a/pkg/rules/validator.go b/pkg/rules/validator.go new file mode 100644 index 0000000..8cb1915 --- /dev/null +++ b/pkg/rules/validator.go @@ -0,0 +1,10 @@ +package rules + +import "fmt" + +func (r *Rule) Validate() error { + if r.Type == "" || r.Payload == "" || r.Target == "" { + return fmt.Errorf("invalid rule: %+v", r) + } + return nil +} diff --git a/pkg/service/service.go b/pkg/service/service.go new file mode 100644 index 0000000..668975b --- /dev/null +++ b/pkg/service/service.go @@ -0,0 +1,123 @@ +package service + +import ( + "sync" + + "github.com/perfect-panel/ppanel-server/pkg/proc" + "github.com/perfect-panel/ppanel-server/pkg/threading" +) + +type ( + // Starter is the interface wraps the Start method. + Starter interface { + Start() + } + + // Stopper is the interface wraps the Stop method. + Stopper interface { + Stop() + } + + // Service is the interface that groups Start and Stop methods. + Service interface { + Starter + Stopper + } + + // Group A ServiceGroup is a group of services. + // Attention: the starting order of the added services is not guaranteed. + Group struct { + services []Service + stopOnce func() + } +) + +// NewServiceGroup returns a ServiceGroup. +func NewServiceGroup() *Group { + sg := new(Group) + sg.stopOnce = Once(sg.doStop) + return sg +} + +// Add adds service into sg. +func (sg *Group) Add(service Service) { + // push front, stop with reverse order. + sg.services = append([]Service{service}, sg.services...) +} + +// Start starts the ServiceGroup. +// There should not be any logic code after calling this method, because this method is a blocking one. +// Also, quitting this method will close the logx output. +func (sg *Group) Start() { + proc.AddShutdownListener(func() { + sg.stopOnce() + }) + + sg.doStart() +} + +// Stop stops the ServiceGroup. +func (sg *Group) Stop() { + sg.stopOnce() +} + +func (sg *Group) doStart() { + routineGroup := threading.NewRoutineGroup() + + for i := range sg.services { + service := sg.services[i] + routineGroup.Run(func() { + service.Start() + }) + } + + routineGroup.Wait() +} + +func (sg *Group) doStop() { + for _, service := range sg.services { + service.Stop() + } +} + +// WithStart wraps a start func as a Service. +func WithStart(start func()) Service { + return startOnlyService{ + start: start, + } +} + +// WithStarter wraps a Starter as a Service. +func WithStarter(start Starter) Service { + return starterOnlyService{ + Starter: start, + } +} + +type ( + stopper struct{} + + startOnlyService struct { + start func() + stopper + } + + starterOnlyService struct { + Starter + stopper + } +) + +func (s stopper) Stop() { +} + +func (s startOnlyService) Start() { + s.start() +} + +func Once(fn func()) func() { + once := new(sync.Once) + return func() { + once.Do(fn) + } +} diff --git a/pkg/service/servicegroup_test.go b/pkg/service/servicegroup_test.go new file mode 100644 index 0000000..8ade90a --- /dev/null +++ b/pkg/service/servicegroup_test.go @@ -0,0 +1,129 @@ +package service + +import ( + "sync" + "testing" + + "github.com/perfect-panel/ppanel-server/pkg/proc" + + "github.com/stretchr/testify/assert" +) + +var ( + number = 1 + mutex sync.Mutex + done = make(chan struct{}) +) + +func TestServiceGroup(t *testing.T) { + multipliers := []int{2, 3, 5, 7} + want := 1 + + group := NewServiceGroup() + for _, multiplier := range multipliers { + want *= multiplier + service := newMockedService(multiplier) + group.Add(service) + } + + go group.Start() + + for i := 0; i < len(multipliers); i++ { + <-done + } + + group.Stop() + proc.Shutdown() + + mutex.Lock() + defer mutex.Unlock() + assert.Equal(t, want, number) +} + +func TestServiceGroup_WithStart(t *testing.T) { + multipliers := []int{2, 3, 5, 7} + want := 1 + + var wait sync.WaitGroup + var lock sync.Mutex + wait.Add(len(multipliers)) + group := NewServiceGroup() + for _, multiplier := range multipliers { + mul := multiplier + group.Add(WithStart(func() { + lock.Lock() + want *= mul + lock.Unlock() + wait.Done() + })) + } + + go group.Start() + wait.Wait() + group.Stop() + + lock.Lock() + defer lock.Unlock() + assert.Equal(t, 210, want) +} + +func TestServiceGroup_WithStarter(t *testing.T) { + multipliers := []int{2, 3, 5, 7} + want := 1 + + var wait sync.WaitGroup + var lock sync.Mutex + wait.Add(len(multipliers)) + group := NewServiceGroup() + for _, multiplier := range multipliers { + mul := multiplier + group.Add(WithStarter(mockedStarter{ + fn: func() { + lock.Lock() + want *= mul + lock.Unlock() + wait.Done() + }, + })) + } + + go group.Start() + wait.Wait() + group.Stop() + + lock.Lock() + defer lock.Unlock() + assert.Equal(t, 210, want) +} + +type mockedStarter struct { + fn func() +} + +func (s mockedStarter) Start() { + s.fn() +} + +type mockedService struct { + quit chan struct{} + multiplier int +} + +func newMockedService(multiplier int) *mockedService { + return &mockedService{ + quit: make(chan struct{}), + multiplier: multiplier, + } +} + +func (s *mockedService) Start() { + mutex.Lock() + number *= s.multiplier + mutex.Unlock() + done <- struct{}{} + <-s.quit +} + +func (s *mockedService) Stop() { + close(s.quit) +} diff --git a/pkg/sms/abosend/abosend.go b/pkg/sms/abosend/abosend.go new file mode 100644 index 0000000..4701b79 --- /dev/null +++ b/pkg/sms/abosend/abosend.go @@ -0,0 +1,101 @@ +package abosend + +import ( + "encoding/json" + "fmt" + + "github.com/go-resty/resty/v2" + "github.com/perfect-panel/ppanel-server/pkg/random" + "github.com/perfect-panel/ppanel-server/pkg/templatex" + "github.com/perfect-panel/ppanel-server/pkg/tool" +) + +const BaseURL = "https://smsapi.abosend.com" + +type Config struct { + ApiDomain string `json:"api_domain"` + Access string `json:"access"` + Secret string `json:"secret"` + Template string `json:"template"` +} + +type Client struct { + config *Config + client *resty.Client +} + +type request struct { + OrgCode string `json:"orgCode"` + MobileArea string `json:"mobileArea"` + Mobile string `json:"mobiles"` + Content string `json:"content"` + Rand string `json:"rand"` + Sign string `json:"sign"` +} + +type response struct { + Code int `json:"code"` + Message string `json:"message"` + Data struct { + SendCode string `json:"sendCode"` + } +} + +func (l *response) Unmarshal(data []byte) error { + return json.Unmarshal(data, &l) +} + +func NewClient(config Config) *Client { + client := resty.New() + if config.ApiDomain != "" { + client.SetBaseURL(config.ApiDomain) + } else { + client.SetBaseURL(BaseURL) + } + return &Client{ + config: &config, + client: client, + } +} + +func (c *Client) SendCode(area, mobile, code string) error { + text, err := templatex.RenderToString(c.config.Template, map[string]interface{}{ + "code": code, + }) + if err != nil { + return fmt.Errorf("failed to render sms template: %s", err.Error()) + } + randNumber := random.Key(6, 0) + sign := tool.Md5Encode(fmt.Sprintf("%s%s%s%s", c.config.Access, text, randNumber, c.config.Secret), true) + req := request{ + OrgCode: c.config.Access, + MobileArea: fmt.Sprintf("+%s", area), + Mobile: fmt.Sprintf("%s%s", area, mobile), + Content: text, + Rand: randNumber, + Sign: sign, + } + resp, err := c.client.R().SetBody(req).ForceContentType("application/json").Post("/v2/api/sendSMS") + if err != nil { + return err + } + if resp.StatusCode() != 200 { + return fmt.Errorf("send sms failed, status code: %d", resp.StatusCode()) + } + var result response + err = result.Unmarshal(resp.Body()) + if err != nil { + return fmt.Errorf("failed to unmarshal response: %s", err.Error()) + } + if result.Code != 200 { + return fmt.Errorf("send sms failed, code: %d, msg: %s", result.Code, result.Message) + } + return nil +} + +func (c *Client) GetSendCodeContent(code string) string { + text, _ := templatex.RenderToString(c.config.Template, map[string]interface{}{ + "code": code, + }) + return text +} diff --git a/pkg/sms/abosend/abosend_test.go b/pkg/sms/abosend/abosend_test.go new file mode 100644 index 0000000..2002ba0 --- /dev/null +++ b/pkg/sms/abosend/abosend_test.go @@ -0,0 +1,30 @@ +package abosend + +import "testing" + +func TestNewClient(t *testing.T) { + t.Skipf("Skip TestNewClient test") + client := createClient() + err := client.SendCode("1", "", "223322") + if err != nil { + t.Errorf("TestNewClient() error = %v", err.Error()) + return + } + t.Logf("TestNewClient success") +} + +func TestClient_GetSendCodeContent(t *testing.T) { + t.Skipf("Skip TestClient_GetSendCodeContent test") + client := createClient() + content := client.GetSendCodeContent("223322") + t.Logf("TestClient_GetSendCodeContent() = %v", content) +} + +func createClient() *Client { + return NewClient(Config{ + ApiDomain: "https://smsapi.abosend.com", + Access: "", + Secret: "", + Template: "您的验证码是:{{.code}}。请不要把验证码泄露给其他人。", + }) +} diff --git a/pkg/sms/alibabacloud/alibabacloud.go b/pkg/sms/alibabacloud/alibabacloud.go new file mode 100644 index 0000000..992772f --- /dev/null +++ b/pkg/sms/alibabacloud/alibabacloud.go @@ -0,0 +1,76 @@ +package alibabacloud + +import ( + "encoding/json" + "fmt" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + openapi "github.com/alibabacloud-go/darabonba-openapi/client" + dysmsapi "github.com/alibabacloud-go/dysmsapi-20170525/v2/client" + "github.com/alibabacloud-go/tea/tea" +) + +type Config struct { + Access string `json:"access"` + Secret string `json:"secret"` + SignName string `json:"sign_name"` + Endpoint string `json:"endpoint"` + TemplateCode string `json:"template_code"` +} + +type Client struct { + config *Config + client *dysmsapi.Client +} + +func NewClient(config Config) *Client { + client, err := initApiClient(config) + if err != nil { + logger.Error("NewClient: init Alibaba Cloud Api Client failed", logger.Field("error", err.Error())) + } + return &Client{ + config: &config, + client: client, + } +} + +func (c *Client) SendCode(area, mobile, code string) error { + if c.client == nil { + return fmt.Errorf("alibaba cloud api client is nil") + } + jsonCode, _ := json.Marshal(map[string]interface{}{ + "code": code, + }) + sendSmsRequest := &dysmsapi.SendSmsRequest{ + PhoneNumbers: tea.String(fmt.Sprintf("%s%s", area, mobile)), + TemplateCode: tea.String(c.config.TemplateCode), + SignName: tea.String(c.config.SignName), + TemplateParam: tea.String(string(jsonCode)), + } + sendSmsResponse, err := c.client.SendSms(sendSmsRequest) + if err != nil { + return err + } + if *sendSmsResponse.Body.Code != "OK" { + return fmt.Errorf("alibaba cloud send sms failed, code: %s, message: %s", *sendSmsResponse.Body.Code, *sendSmsResponse.Body.Message) + } + return nil +} + +func initApiClient(config Config) (*dysmsapi.Client, error) { + cfg := &openapi.Config{ + AccessKeyId: tea.String(config.Access), + AccessKeySecret: tea.String(config.Secret), + Endpoint: tea.String(config.Endpoint), + } + if config.Endpoint == "" { + cfg.Endpoint = tea.String("dysmsapi.ap-southeast-1.aliyuncs.com") + } + result, err := dysmsapi.NewClient(cfg) + return result, err +} + +func (c *Client) GetSendCodeContent(code string) string { + return fmt.Sprintf("TemplateId: %s, SignName:%s, Code: %s", c.config.TemplateCode, c.config.SignName, code) +} diff --git a/pkg/sms/platform.go b/pkg/sms/platform.go new file mode 100644 index 0000000..ef22cde --- /dev/null +++ b/pkg/sms/platform.go @@ -0,0 +1,85 @@ +package sms + +import "github.com/perfect-panel/ppanel-server/internal/types" + +type Platform int + +const ( + AlibabaCloud Platform = iota + Smsbao + Abosend + Twilio + unsupported +) + +var platformNames = map[string]Platform{ + "AlibabaCloud": AlibabaCloud, + "smsbao": Smsbao, + "abosend": Abosend, + "twilio": Twilio, + "unsupported": unsupported, +} + +func (p Platform) String() string { + for k, v := range platformNames { + if v == p { + return k + } + } + return "unsupported" +} + +func parsePlatform(s string) Platform { + if p, ok := platformNames[s]; ok { + return p + } + return unsupported +} + +func GetSupportedPlatforms() []types.PlatformInfo { + return []types.PlatformInfo{ + { + Platform: AlibabaCloud.String(), + PlatformUrl: "https://www.alibabacloud.com", + PlatformFieldDescription: map[string]string{ + "access": "AccessKeyId", + "secret": "AccessKeySecret", + "template_code": "TemplateCode", + "sign_name": "SignName", + "endpoint": "Endpoint", + }, + }, + { + Platform: Smsbao.String(), + PlatformUrl: "https://www.smsbao.com", + PlatformFieldDescription: map[string]string{ + "access": "Username", + "secret": "Password", + "code_variable": "{{.code}}", + "template": "Your verification code is: {{.code}}", + }, + }, + { + Platform: Abosend.String(), + PlatformUrl: "https://www.abosend.com", + PlatformFieldDescription: map[string]string{ + "access": "OrgCode", + "secret": "MD5Key", + "code_variable": "{{.code}}", + "template": "Your verification code is: {{.code}}", + "api_domain": "https://smsapi.abosend.com", + }, + }, + { + Platform: Twilio.String(), + PlatformUrl: "https://www.twilio.com", + PlatformFieldDescription: map[string]string{ + "access": "AccessSID", + "secret": "AuthToken", + "phone_number": "Sending phone number", + "code_variable": "{{.code}}", + "template": "Your verification code is: {{.code}}", + }, + }, + } +} diff --git a/pkg/sms/sender.go b/pkg/sms/sender.go new file mode 100644 index 0000000..30c2a72 --- /dev/null +++ b/pkg/sms/sender.go @@ -0,0 +1,49 @@ +package sms + +import ( + "encoding/json" + "fmt" + "log" + + "github.com/perfect-panel/ppanel-server/pkg/sms/abosend" + "github.com/perfect-panel/ppanel-server/pkg/sms/alibabacloud" + "github.com/perfect-panel/ppanel-server/pkg/sms/smsbao" + "github.com/perfect-panel/ppanel-server/pkg/sms/twilio" +) + +type Sender interface { + SendCode(area, mobile, code string) error + GetSendCodeContent(code string) string +} + +func NewSender(platform, config string) (Sender, error) { + log.Printf("platform: %s, config: %s", platform, config) + switch parsePlatform(platform) { + case AlibabaCloud: + cfg := alibabacloud.Config{} + if err := json.Unmarshal([]byte(config), &cfg); err != nil { + return nil, fmt.Errorf("alibabacloud config unmarshal failed: %v", err.Error()) + } + return alibabacloud.NewClient(cfg), nil + case Abosend: + cfg := abosend.Config{} + if err := json.Unmarshal([]byte(config), &cfg); err != nil { + return nil, fmt.Errorf("abosend config unmarshal failed: %v", err.Error()) + } + return abosend.NewClient(cfg), nil + case Smsbao: + cfg := smsbao.Config{} + if err := json.Unmarshal([]byte(config), &cfg); err != nil { + return nil, fmt.Errorf("smsbao config unmarshal failed: %v", err.Error()) + } + return smsbao.NewClient(cfg), nil + case Twilio: + cfg := twilio.Config{} + if err := json.Unmarshal([]byte(config), &cfg); err != nil { + return nil, fmt.Errorf("twilio config unmarshal failed: %v", err.Error()) + } + return twilio.NewClient(cfg), nil + default: + return nil, fmt.Errorf("unsupported platform: %s", platform) + } +} diff --git a/pkg/sms/smsbao/error.go b/pkg/sms/smsbao/error.go new file mode 100644 index 0000000..5e3bdbb --- /dev/null +++ b/pkg/sms/smsbao/error.go @@ -0,0 +1,58 @@ +package smsbao + +import "fmt" + +type Error int + +const ( + Success Error = iota + PasswordError + AccountNotFount + InsufficientBalance + IPAddressRestrictions + ContentContainsSensitiveWords + MobileNumberIsIncorrect +) + +var errorDescriptions = map[Error]string{ + Success: "Success", + PasswordError: "Password error", + AccountNotFount: "Account not found", + InsufficientBalance: "Insufficient balance", + IPAddressRestrictions: "IP address restrictions", + ContentContainsSensitiveWords: "Content contains sensitive words", + MobileNumberIsIncorrect: "Mobile number is incorrect", +} + +var errorCodes = map[string]Error{ + "0": Success, + "30": PasswordError, + "40": AccountNotFount, + "41": InsufficientBalance, + "43": IPAddressRestrictions, + "50": ContentContainsSensitiveWords, + "51": MobileNumberIsIncorrect, +} + +func (e Error) String() string { + for k, v := range errorDescriptions { + if k == e { + return v + } + } + return "Unknown error" +} + +func parseError(b []byte) error { + if e, ok := errorCodes[string(b)]; ok { + return e.Error() + } + return fmt.Errorf("unknown error") +} + +func (e Error) Error() error { + if e == Success { + return nil + } + return fmt.Errorf("%s", e.String()) +} diff --git a/pkg/sms/smsbao/smsbao.go b/pkg/sms/smsbao/smsbao.go new file mode 100644 index 0000000..20a9f4b --- /dev/null +++ b/pkg/sms/smsbao/smsbao.go @@ -0,0 +1,67 @@ +package smsbao + +import ( + "fmt" + + "github.com/go-resty/resty/v2" + "github.com/perfect-panel/ppanel-server/pkg/templatex" + "github.com/perfect-panel/ppanel-server/pkg/tool" +) + +const BaseURL = "https://api.smsbao.com" + +type Config struct { + Access string `json:"access"` + Secret string `json:"secret"` + Template string `json:"template"` +} + +type Client struct { + config *Config + client *resty.Client +} + +func NewClient(config Config) *Client { + client := resty.New() + client.SetBaseURL(BaseURL) + return &Client{ + config: &config, + client: client, + } +} + +func (c *Client) SendCode(area, mobile, code string) error { + apiUrl := "/sms" + text, err := templatex.RenderToString(c.config.Template, map[string]interface{}{ + "code": code, + }) + if err != nil { + return fmt.Errorf("failed to render sms template: %s", err.Error()) + } + param := map[string]string{ + "u": c.config.Access, + "p": tool.Md5Encode(c.config.Secret, false), + "m": mobile, + "c": text, + } + if area != "86" { + apiUrl = "/wsms" + param["m"] = fmt.Sprintf("+%s%s", area, mobile) + } + resp, err := c.client.R().SetQueryParams(param).Get(apiUrl) + if err != nil { + return err + } + err = parseError(resp.Body()) + if err != nil { + return err + } + return nil +} + +func (c *Client) GetSendCodeContent(code string) string { + text, _ := templatex.RenderToString(c.config.Template, map[string]interface{}{ + "code": code, + }) + return text +} diff --git a/pkg/sms/smsbao/smsbao_test.go b/pkg/sms/smsbao/smsbao_test.go new file mode 100644 index 0000000..800dfcd --- /dev/null +++ b/pkg/sms/smsbao/smsbao_test.go @@ -0,0 +1,16 @@ +package smsbao + +import "testing" + +func TestNewClient(t *testing.T) { + t.Skipf("Skip TestNewClient test") + client := NewClient(Config{ + Template: "【XXX】您的验证码是:{{.code}},有效期 {{.expiration}}。请不要把验证码泄露给其他人。", + }) + err := client.SendCode("1", "", "223322") + if err != nil { + t.Errorf("TestNewClient() error = %v", err.Error()) + return + } + t.Logf("TestNewClient success") +} diff --git a/pkg/sms/twilio/twilio.go b/pkg/sms/twilio/twilio.go new file mode 100644 index 0000000..bb5252d --- /dev/null +++ b/pkg/sms/twilio/twilio.go @@ -0,0 +1,63 @@ +package twilio + +import ( + "fmt" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/templatex" + "github.com/twilio/twilio-go" + twilioApi "github.com/twilio/twilio-go/rest/api/v2010" +) + +type Config struct { + Access string `json:"access"` + Secret string `json:"secret"` + PhoneNumber string `json:"phone_number"` + Template string `json:"template"` +} + +type Client struct { + config Config + client *twilio.RestClient +} + +func NewClient(config Config) *Client { + client := twilio.NewRestClientWithParams(twilio.ClientParams{ + Username: config.Access, + Password: config.Secret, + }) + return &Client{ + config: config, + client: client, + } +} + +func (c *Client) SendCode(area, mobile, code string) error { + params := &twilioApi.CreateMessageParams{} + params.SetTo(fmt.Sprintf("+%s%s", area, mobile)) + params.SetFrom(c.config.PhoneNumber) + text, err := templatex.RenderToString(c.config.Template, map[string]interface{}{ + "code": code, + }) + if err != nil { + logger.Error("twilio send code render template error", logger.Field("error", err.Error()), logger.Field("template", c.config.Template), logger.Field("code", code)) + } + params.SetBody(text) + resp, err := c.client.Api.CreateMessage(params) + if err != nil { + logger.Error("twilio send code error", logger.Field("error", err.Error()), logger.Field("params", params)) + return fmt.Errorf("twilio send code error: %s", err.Error()) + } + if resp.ErrorCode != nil { + logger.Error("twilio send code error", logger.Field("error_code", *resp.ErrorCode), logger.Field("error_message", *resp.ErrorMessage)) + return fmt.Errorf("twilio send code error: %s", *resp.ErrorMessage) + } + return nil +} + +func (c *Client) GetSendCodeContent(code string) string { + text, _ := templatex.RenderToString(c.config.Template, map[string]interface{}{ + "code": code, + }) + return text +} diff --git a/pkg/sms/twilio/twilio_test.go b/pkg/sms/twilio/twilio_test.go new file mode 100644 index 0000000..3a56509 --- /dev/null +++ b/pkg/sms/twilio/twilio_test.go @@ -0,0 +1,16 @@ +package twilio + +import "testing" + +func TestClient_SendCode(t *testing.T) { + t.Skipf("Skip TestClient_SendCode test") + client := NewClient(Config{ + Access: "", Secret: "", PhoneNumber: "", Template: "", + }) + err := client.SendCode("", "", "123456") + if err != nil { + t.Errorf("SendCode() error = %v", err.Error()) + return + } + t.Logf("SendCode() success") +} diff --git a/pkg/snowflake/snowflake.go b/pkg/snowflake/snowflake.go new file mode 100644 index 0000000..d2984d7 --- /dev/null +++ b/pkg/snowflake/snowflake.go @@ -0,0 +1,64 @@ +package snowflake + +import ( + "errors" + "net" + + sf "github.com/GUAIK-ORG/go-snowflake/snowflake" +) + +var snowflake *sf.Snowflake + +func init() { + localIp3, err := GetLocalIp() + if err != nil { + panic(err) + } + dataCenterID := int64(localIp3 >> 4) + workerID := int64(localIp3 & 0xf) + snowflake, err = sf.NewSnowflake(dataCenterID, workerID) + if err != nil { + panic(err) + } +} + +func GetID() int64 { + return snowflake.NextVal() +} + +func GetTimestamp(id int64) int64 { + return sf.GetTimestamp(id) +} + +func GetGenTimestamp(id int64) int64 { + return sf.GetGenTimestamp(id) +} + +func GetLocalIp() (byte, error) { + ifaces, err := net.Interfaces() + if err != nil { + return 0, err + } + + for _, i := range ifaces { + addrs, errRet := i.Addrs() + if errRet != nil { + continue + } + + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + if v.IP.IsGlobalUnicast() { + ip = v.IP.To4() + if ip != nil { + return ip[3], nil + } + } + } + } + } + + return 0, errors.New("no validate ifaces to IPV4") +} diff --git a/pkg/snowflake/snowflake_test.go b/pkg/snowflake/snowflake_test.go new file mode 100644 index 0000000..8ebecbf --- /dev/null +++ b/pkg/snowflake/snowflake_test.go @@ -0,0 +1,59 @@ +package snowflake + +import ( + "testing" +) + +func TestGetLocalIp(t *testing.T) { + tests := []struct { + name string + // want byte + wantErr bool + wantFunc func(byte) (bool, string) + }{ + // TODO: Add test cases. + { + name: "", + wantErr: false, + wantFunc: func(got byte) (bool, string) { return got > 0, "got > 0" }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetLocalIp() + if (err != nil) != tt.wantErr { + t.Errorf("GetLocalIp() error = %v, wantErr %v", err, tt.wantErr) + return + } + if r, s := tt.wantFunc(got); !r { + t.Errorf("GetLocalIp() = %v, want %v", got, s) + } + }) + } +} + +func TestGetID(t *testing.T) { + tests := []struct { + name string + // want int64 + wantErr bool + wantFunc func(int64) (bool, string) + }{ + // TODO: Add test cases. + { + name: "", + wantErr: false, + wantFunc: func(got int64) (bool, string) { + return got > 0, "got > 0" + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := GetID() + if r, s := tt.wantFunc(got); !r { + t.Errorf("GetID() = %v, want %v", got, s) + } + }) + } +} diff --git a/pkg/syncx/atomicbool.go b/pkg/syncx/atomicbool.go new file mode 100644 index 0000000..751b830 --- /dev/null +++ b/pkg/syncx/atomicbool.go @@ -0,0 +1,46 @@ +package syncx + +import "sync/atomic" + +// An AtomicBool is an atomic implementation for boolean values. +type AtomicBool uint32 + +// NewAtomicBool returns an AtomicBool. +func NewAtomicBool() *AtomicBool { + return new(AtomicBool) +} + +// ForAtomicBool returns an AtomicBool with given val. +func ForAtomicBool(val bool) *AtomicBool { + b := NewAtomicBool() + b.Set(val) + return b +} + +// CompareAndSwap compares current value with given old, if equals, set to given val. +func (b *AtomicBool) CompareAndSwap(old, val bool) bool { + var ov, nv uint32 + + if old { + ov = 1 + } + if val { + nv = 1 + } + + return atomic.CompareAndSwapUint32((*uint32)(b), ov, nv) +} + +// Set sets the value to v. +func (b *AtomicBool) Set(v bool) { + if v { + atomic.StoreUint32((*uint32)(b), 1) + } else { + atomic.StoreUint32((*uint32)(b), 0) + } +} + +// True returns true if current value is true. +func (b *AtomicBool) True() bool { + return atomic.LoadUint32((*uint32)(b)) == 1 +} diff --git a/pkg/syncx/atomicbool_test.go b/pkg/syncx/atomicbool_test.go new file mode 100644 index 0000000..f1f8557 --- /dev/null +++ b/pkg/syncx/atomicbool_test.go @@ -0,0 +1,27 @@ +package syncx + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAtomicBool(t *testing.T) { + val := ForAtomicBool(true) + assert.True(t, val.True()) + val.Set(false) + assert.False(t, val.True()) + val.Set(true) + assert.True(t, val.True()) + val.Set(false) + assert.False(t, val.True()) + ok := val.CompareAndSwap(false, true) + assert.True(t, ok) + assert.True(t, val.True()) + ok = val.CompareAndSwap(true, false) + assert.True(t, ok) + assert.False(t, val.True()) + ok = val.CompareAndSwap(true, false) + assert.False(t, ok) + assert.False(t, val.True()) +} diff --git a/pkg/syncx/atomicduration.go b/pkg/syncx/atomicduration.go new file mode 100644 index 0000000..3ae12c6 --- /dev/null +++ b/pkg/syncx/atomicduration.go @@ -0,0 +1,36 @@ +package syncx + +import ( + "sync/atomic" + "time" +) + +// An AtomicDuration is an implementation of atomic duration. +type AtomicDuration int64 + +// NewAtomicDuration returns an AtomicDuration. +func NewAtomicDuration() *AtomicDuration { + return new(AtomicDuration) +} + +// ForAtomicDuration returns an AtomicDuration with given value. +func ForAtomicDuration(val time.Duration) *AtomicDuration { + d := NewAtomicDuration() + d.Set(val) + return d +} + +// CompareAndSwap compares current value with old, if equals, set the value to val. +func (d *AtomicDuration) CompareAndSwap(old, val time.Duration) bool { + return atomic.CompareAndSwapInt64((*int64)(d), int64(old), int64(val)) +} + +// Load loads the current duration. +func (d *AtomicDuration) Load() time.Duration { + return time.Duration(atomic.LoadInt64((*int64)(d))) +} + +// Set sets the value to val. +func (d *AtomicDuration) Set(val time.Duration) { + atomic.StoreInt64((*int64)(d), int64(val)) +} diff --git a/pkg/syncx/atomicduration_test.go b/pkg/syncx/atomicduration_test.go new file mode 100644 index 0000000..8165e13 --- /dev/null +++ b/pkg/syncx/atomicduration_test.go @@ -0,0 +1,19 @@ +package syncx + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestAtomicDuration(t *testing.T) { + d := ForAtomicDuration(time.Duration(100)) + assert.Equal(t, time.Duration(100), d.Load()) + d.Set(time.Duration(200)) + assert.Equal(t, time.Duration(200), d.Load()) + assert.True(t, d.CompareAndSwap(time.Duration(200), time.Duration(300))) + assert.Equal(t, time.Duration(300), d.Load()) + assert.False(t, d.CompareAndSwap(time.Duration(200), time.Duration(400))) + assert.Equal(t, time.Duration(300), d.Load()) +} diff --git a/pkg/syncx/atomicfloat64.go b/pkg/syncx/atomicfloat64.go new file mode 100644 index 0000000..0ebead8 --- /dev/null +++ b/pkg/syncx/atomicfloat64.go @@ -0,0 +1,47 @@ +package syncx + +import ( + "math" + "sync/atomic" +) + +// An AtomicFloat64 is an implementation of atomic float64. +type AtomicFloat64 uint64 + +// NewAtomicFloat64 returns an AtomicFloat64. +func NewAtomicFloat64() *AtomicFloat64 { + return new(AtomicFloat64) +} + +// ForAtomicFloat64 returns an AtomicFloat64 with given val. +func ForAtomicFloat64(val float64) *AtomicFloat64 { + f := NewAtomicFloat64() + f.Set(val) + return f +} + +// Add adds val to current value. +func (f *AtomicFloat64) Add(val float64) float64 { + for { + old := f.Load() + nv := old + val + if f.CompareAndSwap(old, nv) { + return nv + } + } +} + +// CompareAndSwap compares current value with old, if equals, set the value to val. +func (f *AtomicFloat64) CompareAndSwap(old, val float64) bool { + return atomic.CompareAndSwapUint64((*uint64)(f), math.Float64bits(old), math.Float64bits(val)) +} + +// Load loads the current value. +func (f *AtomicFloat64) Load() float64 { + return math.Float64frombits(atomic.LoadUint64((*uint64)(f))) +} + +// Set sets the current value to val. +func (f *AtomicFloat64) Set(val float64) { + atomic.StoreUint64((*uint64)(f), math.Float64bits(val)) +} diff --git a/pkg/syncx/atomicfloat64_test.go b/pkg/syncx/atomicfloat64_test.go new file mode 100644 index 0000000..c3c5fa1 --- /dev/null +++ b/pkg/syncx/atomicfloat64_test.go @@ -0,0 +1,24 @@ +package syncx + +import ( + "sync" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAtomicFloat64(t *testing.T) { + f := ForAtomicFloat64(100) + var wg sync.WaitGroup + for i := 0; i < 5; i++ { + wg.Add(1) + go func() { + for i := 0; i < 100; i++ { + f.Add(1) + } + wg.Done() + }() + } + wg.Wait() + assert.Equal(t, float64(600), f.Load()) +} diff --git a/pkg/syncx/barrier.go b/pkg/syncx/barrier.go new file mode 100644 index 0000000..0a3c894 --- /dev/null +++ b/pkg/syncx/barrier.go @@ -0,0 +1,20 @@ +package syncx + +import "sync" + +// A Barrier is used to facility the barrier on a resource. +type Barrier struct { + lock sync.Mutex +} + +// Guard guards the given fn on the resource. +func (b *Barrier) Guard(fn func()) { + Guard(&b.lock, fn) +} + +// Guard guards the given fn with lock. +func Guard(lock sync.Locker, fn func()) { + lock.Lock() + defer lock.Unlock() + fn() +} diff --git a/pkg/syncx/barrier_test.go b/pkg/syncx/barrier_test.go new file mode 100644 index 0000000..7e2426e --- /dev/null +++ b/pkg/syncx/barrier_test.go @@ -0,0 +1,56 @@ +package syncx + +import ( + "sync" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBarrier_Guard(t *testing.T) { + const total = 10000 + var barrier Barrier + var count int + var wg sync.WaitGroup + wg.Add(total) + for i := 0; i < total; i++ { + go barrier.Guard(func() { + count++ + wg.Done() + }) + } + wg.Wait() + assert.Equal(t, total, count) +} + +func TestBarrierPtr_Guard(t *testing.T) { + const total = 10000 + barrier := new(Barrier) + var count int + wg := new(sync.WaitGroup) + wg.Add(total) + for i := 0; i < total; i++ { + go barrier.Guard(func() { + count++ + wg.Done() + }) + } + wg.Wait() + assert.Equal(t, total, count) +} + +func TestGuard(t *testing.T) { + const total = 10000 + var count int + var lock sync.Mutex + wg := new(sync.WaitGroup) + wg.Add(total) + for i := 0; i < total; i++ { + go Guard(&lock, func() { + count++ + wg.Done() + }) + } + wg.Wait() + assert.Equal(t, total, count) +} diff --git a/pkg/syncx/cond.go b/pkg/syncx/cond.go new file mode 100644 index 0000000..3c82843 --- /dev/null +++ b/pkg/syncx/cond.go @@ -0,0 +1,49 @@ +package syncx + +import ( + "time" + + "github.com/perfect-panel/ppanel-server/pkg/lang" + "github.com/perfect-panel/ppanel-server/pkg/timex" +) + +// A Cond is used to wait for conditions. +type Cond struct { + signal chan lang.PlaceholderType +} + +// NewCond returns a Cond. +func NewCond() *Cond { + return &Cond{ + signal: make(chan lang.PlaceholderType), + } +} + +// WaitWithTimeout wait for signal return remain wait time or timed out. +func (cond *Cond) WaitWithTimeout(timeout time.Duration) (time.Duration, bool) { + timer := time.NewTimer(timeout) + defer timer.Stop() + + begin := timex.Now() + select { + case <-cond.signal: + elapsed := timex.Since(begin) + remainTimeout := timeout - elapsed + return remainTimeout, true + case <-timer.C: + return 0, false + } +} + +// Wait waits for signals. +func (cond *Cond) Wait() { + <-cond.signal +} + +// Signal wakes one goroutine waiting on c, if there is any. +func (cond *Cond) Signal() { + select { + case cond.signal <- lang.Placeholder: + default: + } +} diff --git a/pkg/syncx/cond_test.go b/pkg/syncx/cond_test.go new file mode 100644 index 0000000..c7e7c28 --- /dev/null +++ b/pkg/syncx/cond_test.go @@ -0,0 +1,69 @@ +package syncx + +import ( + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestTimeoutCondWait(t *testing.T) { + var wait sync.WaitGroup + cond := NewCond() + wait.Add(2) + go func() { + cond.Wait() + wait.Done() + }() + time.Sleep(time.Duration(50) * time.Millisecond) + go func() { + cond.Signal() + wait.Done() + }() + wait.Wait() +} + +func TestTimeoutCondWaitTimeout(t *testing.T) { + var wait sync.WaitGroup + cond := NewCond() + wait.Add(1) + go func() { + cond.WaitWithTimeout(time.Duration(500) * time.Millisecond) + wait.Done() + }() + wait.Wait() +} + +func TestTimeoutCondWaitTimeoutRemain(t *testing.T) { + var wait sync.WaitGroup + cond := NewCond() + wait.Add(2) + ch := make(chan time.Duration, 1) + defer close(ch) + timeout := time.Duration(2000) * time.Millisecond + go func() { + remainTimeout, _ := cond.WaitWithTimeout(timeout) + ch <- remainTimeout + wait.Done() + }() + sleep(200) + go func() { + cond.Signal() + wait.Done() + }() + wait.Wait() + remainTimeout := <-ch + assert.True(t, remainTimeout < timeout, "expect remainTimeout %v < %v", remainTimeout, timeout) + assert.True(t, remainTimeout >= time.Duration(200)*time.Millisecond, + "expect remainTimeout %v >= 200 millisecond", remainTimeout) +} + +func TestSignalNoWait(t *testing.T) { + cond := NewCond() + cond.Signal() +} + +func sleep(millisecond int) { + time.Sleep(time.Duration(millisecond) * time.Millisecond) +} diff --git a/pkg/syncx/donechan.go b/pkg/syncx/donechan.go new file mode 100644 index 0000000..c748f53 --- /dev/null +++ b/pkg/syncx/donechan.go @@ -0,0 +1,32 @@ +package syncx + +import ( + "sync" + + "github.com/perfect-panel/ppanel-server/pkg/lang" +) + +// A DoneChan is used as a channel that can be closed multiple times and wait for done. +type DoneChan struct { + done chan lang.PlaceholderType + once sync.Once +} + +// NewDoneChan returns a DoneChan. +func NewDoneChan() *DoneChan { + return &DoneChan{ + done: make(chan lang.PlaceholderType), + } +} + +// Close closes dc, it's safe to close more than once. +func (dc *DoneChan) Close() { + dc.once.Do(func() { + close(dc.done) + }) +} + +// Done returns a channel that can be notified on dc closed. +func (dc *DoneChan) Done() chan lang.PlaceholderType { + return dc.done +} diff --git a/pkg/syncx/donechan_test.go b/pkg/syncx/donechan_test.go new file mode 100644 index 0000000..2db0f15 --- /dev/null +++ b/pkg/syncx/donechan_test.go @@ -0,0 +1,31 @@ +package syncx + +import ( + "sync" + "testing" +) + +func TestDoneChanClose(t *testing.T) { + doneChan := NewDoneChan() + + for i := 0; i < 5; i++ { + doneChan.Close() + } +} + +func TestDoneChanDone(t *testing.T) { + var waitGroup sync.WaitGroup + doneChan := NewDoneChan() + + waitGroup.Add(1) + go func() { + <-doneChan.Done() + waitGroup.Done() + }() + + for i := 0; i < 5; i++ { + doneChan.Close() + } + + waitGroup.Wait() +} diff --git a/pkg/syncx/immutableresource.go b/pkg/syncx/immutableresource.go new file mode 100644 index 0000000..d207d07 --- /dev/null +++ b/pkg/syncx/immutableresource.go @@ -0,0 +1,82 @@ +package syncx + +import ( + "sync" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/timex" +) + +const defaultRefreshInterval = time.Second + +type ( + // ImmutableResourceOption defines the method to customize an ImmutableResource. + ImmutableResourceOption func(resource *ImmutableResource) + + // An ImmutableResource is used to manage an immutable resource. + ImmutableResource struct { + fetch func() (any, error) + resource any + err error + lock sync.RWMutex + refreshInterval time.Duration + lastTime *AtomicDuration + } +) + +// NewImmutableResource returns an ImmutableResource. +func NewImmutableResource(fn func() (any, error), opts ...ImmutableResourceOption) *ImmutableResource { + // cannot use executors.LessExecutor because of cycle imports + ir := ImmutableResource{ + fetch: fn, + refreshInterval: defaultRefreshInterval, + lastTime: NewAtomicDuration(), + } + for _, opt := range opts { + opt(&ir) + } + return &ir +} + +// Get gets the immutable resource, fetches automatically if not loaded. +func (ir *ImmutableResource) Get() (any, error) { + ir.lock.RLock() + resource := ir.resource + ir.lock.RUnlock() + if resource != nil { + return resource, nil + } + + ir.maybeRefresh(func() { + res, err := ir.fetch() + ir.lock.Lock() + if err != nil { + ir.err = err + } else { + ir.resource, ir.err = res, nil + } + ir.lock.Unlock() + }) + + ir.lock.RLock() + resource, err := ir.resource, ir.err + ir.lock.RUnlock() + return resource, err +} + +func (ir *ImmutableResource) maybeRefresh(execute func()) { + now := timex.Now() + lastTime := ir.lastTime.Load() + if lastTime == 0 || lastTime+ir.refreshInterval < now { + ir.lastTime.Set(now) + execute() + } +} + +// WithRefreshIntervalOnFailure sets refresh interval on failure. +// Set interval to 0 to enforce refresh every time if not succeeded, default is time.Second. +func WithRefreshIntervalOnFailure(interval time.Duration) ImmutableResourceOption { + return func(resource *ImmutableResource) { + resource.refreshInterval = interval + } +} diff --git a/pkg/syncx/immutableresource_test.go b/pkg/syncx/immutableresource_test.go new file mode 100644 index 0000000..8aec6b9 --- /dev/null +++ b/pkg/syncx/immutableresource_test.go @@ -0,0 +1,78 @@ +package syncx + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestImmutableResource(t *testing.T) { + var count int + r := NewImmutableResource(func() (any, error) { + count++ + return "hello", nil + }) + + res, err := r.Get() + assert.Equal(t, "hello", res) + assert.Equal(t, 1, count) + assert.Nil(t, err) + + // again + res, err = r.Get() + assert.Equal(t, "hello", res) + assert.Equal(t, 1, count) + assert.Nil(t, err) +} + +func TestImmutableResourceError(t *testing.T) { + var count int + r := NewImmutableResource(func() (any, error) { + count++ + return nil, errors.New("any") + }) + + res, err := r.Get() + assert.Nil(t, res) + assert.NotNil(t, err) + assert.Equal(t, "any", err.Error()) + assert.Equal(t, 1, count) + + // again + res, err = r.Get() + assert.Nil(t, res) + assert.NotNil(t, err) + assert.Equal(t, "any", err.Error()) + assert.Equal(t, 1, count) + + r.refreshInterval = 0 + time.Sleep(time.Millisecond) + res, err = r.Get() + assert.Nil(t, res) + assert.NotNil(t, err) + assert.Equal(t, "any", err.Error()) + assert.Equal(t, 2, count) +} + +func TestImmutableResourceErrorRefreshAlways(t *testing.T) { + var count int + r := NewImmutableResource(func() (any, error) { + count++ + return nil, errors.New("any") + }, WithRefreshIntervalOnFailure(0)) + + res, err := r.Get() + assert.Nil(t, res) + assert.NotNil(t, err) + assert.Equal(t, "any", err.Error()) + assert.Equal(t, 1, count) + + // again + res, err = r.Get() + assert.Nil(t, res) + assert.NotNil(t, err) + assert.Equal(t, "any", err.Error()) + assert.Equal(t, 2, count) +} diff --git a/pkg/syncx/limit.go b/pkg/syncx/limit.go new file mode 100644 index 0000000..5c6ce9e --- /dev/null +++ b/pkg/syncx/limit.go @@ -0,0 +1,48 @@ +package syncx + +import ( + "errors" + + "github.com/perfect-panel/ppanel-server/pkg/lang" +) + +// ErrLimitReturn indicates that the more than borrowed elements were returned. +var ErrLimitReturn = errors.New("discarding limited token, resource pool is full, someone returned multiple times") + +// Limit controls the concurrent requests. +type Limit struct { + pool chan lang.PlaceholderType +} + +// NewLimit creates a Limit that can borrow n elements from it concurrently. +func NewLimit(n int) Limit { + return Limit{ + pool: make(chan lang.PlaceholderType, n), + } +} + +// Borrow borrows an element from Limit in blocking mode. +func (l Limit) Borrow() { + l.pool <- lang.Placeholder +} + +// Return returns the borrowed resource, returns error only if returned more than borrowed. +func (l Limit) Return() error { + select { + case <-l.pool: + return nil + default: + return ErrLimitReturn + } +} + +// TryBorrow tries to borrow an element from Limit, in non-blocking mode. +// If success, true returned, false for otherwise. +func (l Limit) TryBorrow() bool { + select { + case l.pool <- lang.Placeholder: + return true + default: + return false + } +} diff --git a/pkg/syncx/limit_test.go b/pkg/syncx/limit_test.go new file mode 100644 index 0000000..1465dbb --- /dev/null +++ b/pkg/syncx/limit_test.go @@ -0,0 +1,17 @@ +package syncx + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLimit(t *testing.T) { + limit := NewLimit(2) + limit.Borrow() + assert.True(t, limit.TryBorrow()) + assert.False(t, limit.TryBorrow()) + assert.Nil(t, limit.Return()) + assert.Nil(t, limit.Return()) + assert.Equal(t, ErrLimitReturn, limit.Return()) +} diff --git a/pkg/syncx/lockedcalls.go b/pkg/syncx/lockedcalls.go new file mode 100644 index 0000000..ffe47d1 --- /dev/null +++ b/pkg/syncx/lockedcalls.go @@ -0,0 +1,57 @@ +package syncx + +import "sync" + +type ( + // LockedCalls makes sure the calls with the same key to be called sequentially. + // For example, A called F, before it's done, B called F, then B's call would not blocked, + // after A's call finished, B's call got executed. + // The calls with the same key are independent, not sharing the returned values. + // A ------->calls F with key and executes<------->returns + // B ------------------>calls F with key<--------->executes<---->returns + LockedCalls interface { + Do(key string, fn func() (any, error)) (any, error) + } + + lockedGroup struct { + mu sync.Mutex + m map[string]*sync.WaitGroup + } +) + +// NewLockedCalls returns a LockedCalls. +func NewLockedCalls() LockedCalls { + return &lockedGroup{ + m: make(map[string]*sync.WaitGroup), + } +} + +func (lg *lockedGroup) Do(key string, fn func() (any, error)) (any, error) { +begin: + lg.mu.Lock() + if wg, ok := lg.m[key]; ok { + lg.mu.Unlock() + wg.Wait() + goto begin + } + + return lg.makeCall(key, fn) +} + +func (lg *lockedGroup) makeCall(key string, fn func() (any, error)) (any, error) { + var wg sync.WaitGroup + wg.Add(1) + lg.m[key] = &wg + lg.mu.Unlock() + + defer func() { + // delete key first, done later. can't reverse the order, because if reverse, + // another Do call might wg.Wait() without get notified with wg.Done() + lg.mu.Lock() + delete(lg.m, key) + lg.mu.Unlock() + wg.Done() + }() + + return fn() +} diff --git a/pkg/syncx/lockedcalls_test.go b/pkg/syncx/lockedcalls_test.go new file mode 100644 index 0000000..93f6fe0 --- /dev/null +++ b/pkg/syncx/lockedcalls_test.go @@ -0,0 +1,82 @@ +package syncx + +import ( + "errors" + "fmt" + "sync" + "testing" + "time" +) + +func TestLockedCallDo(t *testing.T) { + g := NewLockedCalls() + v, err := g.Do("key", func() (any, error) { + return "bar", nil + }) + if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want { + t.Errorf("Do = %v; want %v", got, want) + } + if err != nil { + t.Errorf("Do error = %v", err) + } +} + +func TestLockedCallDoErr(t *testing.T) { + g := NewLockedCalls() + someErr := errors.New("some error") + v, err := g.Do("key", func() (any, error) { + return nil, someErr + }) + if !errors.Is(err, someErr) { + t.Errorf("Do error = %v; want someErr", err) + } + if v != nil { + t.Errorf("unexpected non-nil value %#v", v) + } +} + +func TestLockedCallDoDupSuppress(t *testing.T) { + g := NewLockedCalls() + c := make(chan string) + var calls int + fn := func() (any, error) { + calls++ + ret := calls + <-c + calls-- + return ret, nil + } + + const n = 10 + var results []int + var lock sync.Mutex + var wg sync.WaitGroup + for i := 0; i < n; i++ { + wg.Add(1) + go func() { + v, err := g.Do("key", fn) + if err != nil { + t.Errorf("Do error: %v", err) + } + + lock.Lock() + results = append(results, v.(int)) + lock.Unlock() + wg.Done() + }() + } + time.Sleep(100 * time.Millisecond) // let goroutines above block + for i := 0; i < n; i++ { + c <- "bar" + } + wg.Wait() + + lock.Lock() + defer lock.Unlock() + + for _, item := range results { + if item != 1 { + t.Errorf("number of calls = %d; want 1", item) + } + } +} diff --git a/pkg/syncx/managedresource.go b/pkg/syncx/managedresource.go new file mode 100644 index 0000000..018e4b5 --- /dev/null +++ b/pkg/syncx/managedresource.go @@ -0,0 +1,48 @@ +package syncx + +import "sync" + +// A ManagedResource is used to manage a resource that might be broken and refetched, like a connection. +type ManagedResource struct { + resource any + lock sync.RWMutex + generate func() any + equals func(a, b any) bool +} + +// NewManagedResource returns a ManagedResource. +func NewManagedResource(generate func() any, equals func(a, b any) bool) *ManagedResource { + return &ManagedResource{ + generate: generate, + equals: equals, + } +} + +// MarkBroken marks the resource broken. +func (mr *ManagedResource) MarkBroken(resource any) { + mr.lock.Lock() + defer mr.lock.Unlock() + + if mr.equals(mr.resource, resource) { + mr.resource = nil + } +} + +// Take takes the resource, if not loaded, generates it. +func (mr *ManagedResource) Take() any { + mr.lock.RLock() + resource := mr.resource + mr.lock.RUnlock() + + if resource != nil { + return resource + } + + mr.lock.Lock() + defer mr.lock.Unlock() + // maybe another Take() call already generated the resource. + if mr.resource == nil { + mr.resource = mr.generate() + } + return mr.resource +} diff --git a/pkg/syncx/managedresource_test.go b/pkg/syncx/managedresource_test.go new file mode 100644 index 0000000..ebcb18d --- /dev/null +++ b/pkg/syncx/managedresource_test.go @@ -0,0 +1,22 @@ +package syncx + +import ( + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestManagedResource(t *testing.T) { + var count int32 + resource := NewManagedResource(func() any { + return atomic.AddInt32(&count, 1) + }, func(a, b any) bool { + return a == b + }) + + assert.Equal(t, resource.Take(), resource.Take()) + old := resource.Take() + resource.MarkBroken(old) + assert.NotEqual(t, old, resource.Take()) +} diff --git a/pkg/syncx/once.go b/pkg/syncx/once.go new file mode 100644 index 0000000..8915eca --- /dev/null +++ b/pkg/syncx/once.go @@ -0,0 +1,11 @@ +package syncx + +import "sync" + +// Once returns a func that guarantees fn can only called once. +func Once(fn func()) func() { + once := new(sync.Once) + return func() { + once.Do(fn) + } +} diff --git a/pkg/syncx/once_test.go b/pkg/syncx/once_test.go new file mode 100644 index 0000000..9e7fd71 --- /dev/null +++ b/pkg/syncx/once_test.go @@ -0,0 +1,33 @@ +package syncx + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOnce(t *testing.T) { + var v int + add := Once(func() { + v++ + }) + + for i := 0; i < 5; i++ { + add() + } + + assert.Equal(t, 1, v) +} + +func BenchmarkOnce(b *testing.B) { + var v int + add := Once(func() { + v++ + }) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + add() + } + assert.Equal(b, 1, v) +} diff --git a/pkg/syncx/onceguard.go b/pkg/syncx/onceguard.go new file mode 100644 index 0000000..5f47361 --- /dev/null +++ b/pkg/syncx/onceguard.go @@ -0,0 +1,18 @@ +package syncx + +import "sync/atomic" + +// An OnceGuard is used to make sure a resource can be taken once. +type OnceGuard struct { + done uint32 +} + +// Taken checks if the resource is taken. +func (og *OnceGuard) Taken() bool { + return atomic.LoadUint32(&og.done) == 1 +} + +// Take takes the resource, returns true on success, false for otherwise. +func (og *OnceGuard) Take() bool { + return atomic.CompareAndSwapUint32(&og.done, 0, 1) +} diff --git a/pkg/syncx/onceguard_test.go b/pkg/syncx/onceguard_test.go new file mode 100644 index 0000000..dac7aa3 --- /dev/null +++ b/pkg/syncx/onceguard_test.go @@ -0,0 +1,17 @@ +package syncx + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOnceGuard(t *testing.T) { + var guard OnceGuard + + assert.False(t, guard.Taken()) + assert.True(t, guard.Take()) + assert.True(t, guard.Taken()) + assert.False(t, guard.Take()) + assert.True(t, guard.Taken()) +} diff --git a/pkg/syncx/pool.go b/pkg/syncx/pool.go new file mode 100644 index 0000000..2bccacb --- /dev/null +++ b/pkg/syncx/pool.go @@ -0,0 +1,108 @@ +package syncx + +import ( + "sync" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/timex" +) + +type ( + // PoolOption defines the method to customize a Pool. + PoolOption func(*Pool) + + node struct { + item any + next *node + lastUsed time.Duration + } + + // A Pool is used to pool resources. + // The difference between sync.Pool is that: + // 1. the limit of the resources + // 2. max age of the resources can be set + // 3. the method to destroy resources can be customized + Pool struct { + limit int + created int + maxAge time.Duration + lock sync.Locker + cond *sync.Cond + head *node + create func() any + destroy func(any) + } +) + +// NewPool returns a Pool. +func NewPool(n int, create func() any, destroy func(any), opts ...PoolOption) *Pool { + if n <= 0 { + panic("pool size can't be negative or zero") + } + + lock := new(sync.Mutex) + pool := &Pool{ + limit: n, + lock: lock, + cond: sync.NewCond(lock), + create: create, + destroy: destroy, + } + + for _, opt := range opts { + opt(pool) + } + + return pool +} + +// Get gets a resource. +func (p *Pool) Get() any { + p.lock.Lock() + defer p.lock.Unlock() + + for { + if p.head != nil { + head := p.head + p.head = head.next + if p.maxAge > 0 && head.lastUsed+p.maxAge < timex.Now() { + p.created-- + p.destroy(head.item) + continue + } else { + return head.item + } + } + + if p.created < p.limit { + p.created++ + return p.create() + } + + p.cond.Wait() + } +} + +// Put puts a resource back. +func (p *Pool) Put(x any) { + if x == nil { + return + } + + p.lock.Lock() + defer p.lock.Unlock() + + p.head = &node{ + item: x, + next: p.head, + lastUsed: timex.Now(), + } + p.cond.Signal() +} + +// WithMaxAge returns a function to customize a Pool with given max age. +func WithMaxAge(duration time.Duration) PoolOption { + return func(pool *Pool) { + pool.maxAge = duration + } +} diff --git a/pkg/syncx/pool_test.go b/pkg/syncx/pool_test.go new file mode 100644 index 0000000..35ab59d --- /dev/null +++ b/pkg/syncx/pool_test.go @@ -0,0 +1,115 @@ +package syncx + +import ( + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/lang" + "github.com/stretchr/testify/assert" +) + +const limit = 10 + +func TestPoolGet(t *testing.T) { + stack := NewPool(limit, create, destroy) + ch := make(chan lang.PlaceholderType) + + for i := 0; i < limit; i++ { + var fail AtomicBool + go func() { + v := stack.Get() + if v.(int) != 1 { + fail.Set(true) + } + ch <- lang.Placeholder + }() + + select { + case <-ch: + case <-time.After(time.Second): + t.Fail() + } + + if fail.True() { + t.Fatal("unmatch value") + } + } +} + +func TestPoolPopTooMany(t *testing.T) { + stack := NewPool(limit, create, destroy) + ch := make(chan lang.PlaceholderType, 1) + + for i := 0; i < limit; i++ { + var wait sync.WaitGroup + wait.Add(1) + go func() { + stack.Get() + ch <- lang.Placeholder + wait.Done() + }() + + wait.Wait() + select { + case <-ch: + default: + t.Fail() + } + } + + var waitGroup, pushWait sync.WaitGroup + waitGroup.Add(1) + pushWait.Add(1) + go func() { + pushWait.Done() + stack.Get() + waitGroup.Done() + }() + + pushWait.Wait() + stack.Put(1) + waitGroup.Wait() +} + +func TestPoolPopFirst(t *testing.T) { + var value int32 + stack := NewPool(limit, func() any { + return atomic.AddInt32(&value, 1) + }, destroy) + + for i := 0; i < 100; i++ { + v := stack.Get().(int32) + assert.Equal(t, 1, int(v)) + stack.Put(v) + } +} + +func TestPoolWithMaxAge(t *testing.T) { + var value int32 + stack := NewPool(limit, func() any { + return atomic.AddInt32(&value, 1) + }, destroy, WithMaxAge(time.Millisecond)) + + v1 := stack.Get().(int32) + // put nil should not matter + stack.Put(nil) + stack.Put(v1) + time.Sleep(time.Millisecond * 10) + v2 := stack.Get().(int32) + assert.NotEqual(t, v1, v2) +} + +func TestNewPoolPanics(t *testing.T) { + assert.Panics(t, func() { + NewPool(0, create, destroy) + }) +} + +func create() any { + return 1 +} + +func destroy(_ any) { +} diff --git a/pkg/syncx/refresource.go b/pkg/syncx/refresource.go new file mode 100644 index 0000000..dd59585 --- /dev/null +++ b/pkg/syncx/refresource.go @@ -0,0 +1,53 @@ +package syncx + +import ( + "errors" + "sync" +) + +// ErrUseOfCleaned is an error that indicates using a cleaned resource. +var ErrUseOfCleaned = errors.New("using a cleaned resource") + +// A RefResource is used to reference counting a resource. +type RefResource struct { + lock sync.Mutex + ref int32 + cleaned bool + clean func() +} + +// NewRefResource returns a RefResource. +func NewRefResource(clean func()) *RefResource { + return &RefResource{ + clean: clean, + } +} + +// Use uses the resource with reference count incremented. +func (r *RefResource) Use() error { + r.lock.Lock() + defer r.lock.Unlock() + + if r.cleaned { + return ErrUseOfCleaned + } + + r.ref++ + return nil +} + +// Clean cleans a resource with reference count decremented. +func (r *RefResource) Clean() { + r.lock.Lock() + defer r.lock.Unlock() + + if r.cleaned { + return + } + + r.ref-- + if r.ref == 0 { + r.cleaned = true + r.clean() + } +} diff --git a/pkg/syncx/refresource_test.go b/pkg/syncx/refresource_test.go new file mode 100644 index 0000000..1cc1688 --- /dev/null +++ b/pkg/syncx/refresource_test.go @@ -0,0 +1,27 @@ +package syncx + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRefCleaner(t *testing.T) { + var count int + clean := func() { + count += 1 + } + + cleaner := NewRefResource(clean) + err := cleaner.Use() + assert.Nil(t, err) + err = cleaner.Use() + assert.Nil(t, err) + cleaner.Clean() + cleaner.Clean() + assert.Equal(t, 1, count) + cleaner.Clean() + cleaner.Clean() + assert.Equal(t, 1, count) + assert.Equal(t, ErrUseOfCleaned, cleaner.Use()) +} diff --git a/pkg/syncx/resourcemanager.go b/pkg/syncx/resourcemanager.go new file mode 100644 index 0000000..ae4ae47 --- /dev/null +++ b/pkg/syncx/resourcemanager.go @@ -0,0 +1,78 @@ +package syncx + +import ( + "io" + "sync" + + "github.com/perfect-panel/ppanel-server/pkg/errorx" +) + +// A ResourceManager is a manager that used to manage resources. +type ResourceManager struct { + resources map[string]io.Closer + singleFlight SingleFlight + lock sync.RWMutex +} + +// NewResourceManager returns a ResourceManager. +func NewResourceManager() *ResourceManager { + return &ResourceManager{ + resources: make(map[string]io.Closer), + singleFlight: NewSingleFlight(), + } +} + +// Close closes the manager. +// Don't use the ResourceManager after Close() called. +func (manager *ResourceManager) Close() error { + manager.lock.Lock() + defer manager.lock.Unlock() + + var be errorx.BatchError + for _, resource := range manager.resources { + if err := resource.Close(); err != nil { + be.Add(err) + } + } + + // release resources to avoid using it later + manager.resources = nil + + return be.Err() +} + +// GetResource returns the resource associated with given key. +func (manager *ResourceManager) GetResource(key string, create func() (io.Closer, error)) ( + io.Closer, error) { + val, err := manager.singleFlight.Do(key, func() (any, error) { + manager.lock.RLock() + resource, ok := manager.resources[key] + manager.lock.RUnlock() + if ok { + return resource, nil + } + + resource, err := create() + if err != nil { + return nil, err + } + + manager.lock.Lock() + defer manager.lock.Unlock() + manager.resources[key] = resource + + return resource, nil + }) + if err != nil { + return nil, err + } + + return val.(io.Closer), nil +} + +// Inject injects the resource associated with given key. +func (manager *ResourceManager) Inject(key string, resource io.Closer) { + manager.lock.Lock() + manager.resources[key] = resource + manager.lock.Unlock() +} diff --git a/pkg/syncx/resourcemanager_test.go b/pkg/syncx/resourcemanager_test.go new file mode 100644 index 0000000..725b8d1 --- /dev/null +++ b/pkg/syncx/resourcemanager_test.go @@ -0,0 +1,99 @@ +package syncx + +import ( + "errors" + "io" + "testing" + + "github.com/stretchr/testify/assert" +) + +type dummyResource struct { + age int +} + +func (dr *dummyResource) Close() error { + return errors.New("close") +} + +func TestResourceManager_GetResource(t *testing.T) { + manager := NewResourceManager() + defer manager.Close() + + var age int + for i := 0; i < 10; i++ { + val, err := manager.GetResource("key", func() (io.Closer, error) { + age++ + return &dummyResource{ + age: age, + }, nil + }) + assert.Nil(t, err) + assert.Equal(t, 1, val.(*dummyResource).age) + } +} + +func TestResourceManager_GetResourceError(t *testing.T) { + manager := NewResourceManager() + defer manager.Close() + + for i := 0; i < 10; i++ { + _, err := manager.GetResource("key", func() (io.Closer, error) { + return nil, errors.New("fail") + }) + assert.NotNil(t, err) + } +} + +func TestResourceManager_Close(t *testing.T) { + manager := NewResourceManager() + defer manager.Close() + + for i := 0; i < 10; i++ { + _, err := manager.GetResource("key", func() (io.Closer, error) { + return nil, errors.New("fail") + }) + assert.NotNil(t, err) + } + + if assert.NoError(t, manager.Close()) { + assert.Equal(t, 0, len(manager.resources)) + } +} + +func TestResourceManager_UseAfterClose(t *testing.T) { + manager := NewResourceManager() + defer manager.Close() + + _, err := manager.GetResource("key", func() (io.Closer, error) { + return nil, errors.New("fail") + }) + assert.NotNil(t, err) + if assert.NoError(t, manager.Close()) { + _, err = manager.GetResource("key", func() (io.Closer, error) { + return nil, errors.New("fail") + }) + assert.NotNil(t, err) + + assert.Panics(t, func() { + _, err = manager.GetResource("key", func() (io.Closer, error) { + return &dummyResource{age: 123}, nil + }) + }) + } +} + +func TestResourceManager_Inject(t *testing.T) { + manager := NewResourceManager() + defer manager.Close() + + manager.Inject("key", &dummyResource{ + age: 10, + }) + + val, err := manager.GetResource("key", func() (io.Closer, error) { + return nil, nil + }) + assert.Nil(t, err) + assert.Equal(t, 10, val.(*dummyResource).age) +} diff --git a/pkg/syncx/singleflight.go b/pkg/syncx/singleflight.go new file mode 100644 index 0000000..cbe62c6 --- /dev/null +++ b/pkg/syncx/singleflight.go @@ -0,0 +1,81 @@ +package syncx + +import "sync" + +type ( + // SingleFlight lets the concurrent calls with the same key to share the call result. + // For example, A called F, before it's done, B called F. Then B would not execute F, + // and shared the result returned by F which called by A. + // The calls with the same key are dependent, concurrent calls share the returned values. + // A ------->calls F with key<------------------->returns val + // B --------------------->calls F with key------>returns val + SingleFlight interface { + Do(key string, fn func() (any, error)) (any, error) + DoEx(key string, fn func() (any, error)) (any, bool, error) + } + + call struct { + wg sync.WaitGroup + val any + err error + } + + flightGroup struct { + calls map[string]*call + lock sync.Mutex + } +) + +// NewSingleFlight returns a SingleFlight. +func NewSingleFlight() SingleFlight { + return &flightGroup{ + calls: make(map[string]*call), + } +} + +func (g *flightGroup) Do(key string, fn func() (any, error)) (any, error) { + c, done := g.createCall(key) + if done { + return c.val, c.err + } + + g.makeCall(c, key, fn) + return c.val, c.err +} + +func (g *flightGroup) DoEx(key string, fn func() (any, error)) (val any, fresh bool, err error) { + c, done := g.createCall(key) + if done { + return c.val, false, c.err + } + + g.makeCall(c, key, fn) + return c.val, true, c.err +} + +func (g *flightGroup) createCall(key string) (c *call, done bool) { + g.lock.Lock() + if c, ok := g.calls[key]; ok { + g.lock.Unlock() + c.wg.Wait() + return c, true + } + + c = new(call) + c.wg.Add(1) + g.calls[key] = c + g.lock.Unlock() + + return c, false +} + +func (g *flightGroup) makeCall(c *call, key string, fn func() (any, error)) { + defer func() { + g.lock.Lock() + delete(g.calls, key) + g.lock.Unlock() + c.wg.Done() + }() + + c.val, c.err = fn() +} diff --git a/pkg/syncx/singleflight_test.go b/pkg/syncx/singleflight_test.go new file mode 100644 index 0000000..591c273 --- /dev/null +++ b/pkg/syncx/singleflight_test.go @@ -0,0 +1,141 @@ +package syncx + +import ( + "errors" + "fmt" + "sync" + "sync/atomic" + "testing" + "time" +) + +func TestExclusiveCallDo(t *testing.T) { + g := NewSingleFlight() + v, err := g.Do("key", func() (any, error) { + return "bar", nil + }) + if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want { + t.Errorf("Do = %v; want %v", got, want) + } + if err != nil { + t.Errorf("Do error = %v", err) + } +} + +func TestExclusiveCallDoErr(t *testing.T) { + g := NewSingleFlight() + someErr := errors.New("some error") + v, err := g.Do("key", func() (any, error) { + return nil, someErr + }) + if !errors.Is(err, someErr) { + t.Errorf("Do error = %v; want someErr", err) + } + if v != nil { + t.Errorf("unexpected non-nil value %#v", v) + } +} + +func TestExclusiveCallDoDupSuppress(t *testing.T) { + g := NewSingleFlight() + c := make(chan string) + var calls int32 + fn := func() (any, error) { + atomic.AddInt32(&calls, 1) + return <-c, nil + } + + const n = 10 + var wg sync.WaitGroup + for i := 0; i < n; i++ { + wg.Add(1) + go func() { + v, err := g.Do("key", fn) + if err != nil { + t.Errorf("Do error: %v", err) + } + if v.(string) != "bar" { + t.Errorf("got %q; want %q", v, "bar") + } + wg.Done() + }() + } + time.Sleep(100 * time.Millisecond) // let goroutines above block + c <- "bar" + wg.Wait() + if got := atomic.LoadInt32(&calls); got != 1 { + t.Errorf("number of calls = %d; want 1", got) + } +} + +func TestExclusiveCallDoDiffDupSuppress(t *testing.T) { + g := NewSingleFlight() + broadcast := make(chan struct{}) + var calls int32 + tests := []string{"e", "a", "e", "a", "b", "c", "b", "a", "c", "d", "b", "c", "d"} + + var wg sync.WaitGroup + for _, key := range tests { + wg.Add(1) + go func(k string) { + <-broadcast // get all goroutines ready + _, err := g.Do(k, func() (any, error) { + atomic.AddInt32(&calls, 1) + time.Sleep(10 * time.Millisecond) + return nil, nil + }) + if err != nil { + t.Errorf("Do error: %v", err) + } + wg.Done() + }(key) + } + + time.Sleep(100 * time.Millisecond) // let goroutines above block + close(broadcast) + wg.Wait() + + if got := atomic.LoadInt32(&calls); got != 5 { + // five letters + t.Errorf("number of calls = %d; want 5", got) + } +} + +func TestExclusiveCallDoExDupSuppress(t *testing.T) { + g := NewSingleFlight() + c := make(chan string) + var calls int32 + fn := func() (any, error) { + atomic.AddInt32(&calls, 1) + return <-c, nil + } + + const n = 10 + var wg sync.WaitGroup + var freshes int32 + for i := 0; i < n; i++ { + wg.Add(1) + go func() { + v, fresh, err := g.DoEx("key", fn) + if err != nil { + t.Errorf("Do error: %v", err) + } + if fresh { + atomic.AddInt32(&freshes, 1) + } + if v.(string) != "bar" { + t.Errorf("got %q; want %q", v, "bar") + } + wg.Done() + }() + } + time.Sleep(100 * time.Millisecond) // let goroutines above block + c <- "bar" + wg.Wait() + if got := atomic.LoadInt32(&calls); got != 1 { + t.Errorf("number of calls = %d; want 1", got) + } + if got := atomic.LoadInt32(&freshes); got != 1 { + t.Errorf("freshes = %d; want 1", got) + } +} diff --git a/pkg/syncx/spinlock.go b/pkg/syncx/spinlock.go new file mode 100644 index 0000000..4736be4 --- /dev/null +++ b/pkg/syncx/spinlock.go @@ -0,0 +1,28 @@ +package syncx + +import ( + "runtime" + "sync/atomic" +) + +// A SpinLock is used as a lock a fast execution. +type SpinLock struct { + lock uint32 +} + +// Lock locks the SpinLock. +func (sl *SpinLock) Lock() { + for !sl.TryLock() { + runtime.Gosched() + } +} + +// TryLock tries to lock the SpinLock. +func (sl *SpinLock) TryLock() bool { + return atomic.CompareAndSwapUint32(&sl.lock, 0, 1) +} + +// Unlock unlocks the SpinLock. +func (sl *SpinLock) Unlock() { + atomic.StoreUint32(&sl.lock, 0) +} diff --git a/pkg/syncx/spinlock_test.go b/pkg/syncx/spinlock_test.go new file mode 100644 index 0000000..026617c --- /dev/null +++ b/pkg/syncx/spinlock_test.go @@ -0,0 +1,70 @@ +package syncx + +import ( + "runtime" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/lang" + "github.com/stretchr/testify/assert" +) + +func TestTryLock(t *testing.T) { + var lock SpinLock + assert.True(t, lock.TryLock()) + assert.False(t, lock.TryLock()) + lock.Unlock() + assert.True(t, lock.TryLock()) +} + +func TestSpinLock(t *testing.T) { + var lock SpinLock + lock.Lock() + assert.False(t, lock.TryLock()) + lock.Unlock() + assert.True(t, lock.TryLock()) +} + +func TestSpinLockRace(t *testing.T) { + var lock SpinLock + lock.Lock() + var wait sync.WaitGroup + wait.Add(1) + go func() { + wait.Done() + }() + time.Sleep(time.Millisecond * 100) + lock.Unlock() + wait.Wait() + assert.True(t, lock.TryLock()) +} + +func TestSpinLock_TryLock(t *testing.T) { + var lock SpinLock + var count int32 + var wait sync.WaitGroup + wait.Add(2) + sig := make(chan lang.PlaceholderType) + + go func() { + lock.TryLock() + sig <- lang.Placeholder + atomic.AddInt32(&count, 1) + runtime.Gosched() + lock.Unlock() + wait.Done() + }() + + go func() { + <-sig + lock.Lock() + atomic.AddInt32(&count, 1) + lock.Unlock() + wait.Done() + }() + + wait.Wait() + assert.Equal(t, int32(2), atomic.LoadInt32(&count)) +} diff --git a/pkg/syncx/timeoutlimit.go b/pkg/syncx/timeoutlimit.go new file mode 100644 index 0000000..7fec353 --- /dev/null +++ b/pkg/syncx/timeoutlimit.go @@ -0,0 +1,57 @@ +package syncx + +import ( + "errors" + "time" +) + +// ErrTimeout is an error that indicates the borrow timeout. +var ErrTimeout = errors.New("borrow timeout") + +// A TimeoutLimit is used to borrow with timeouts. +type TimeoutLimit struct { + limit Limit + cond *Cond +} + +// NewTimeoutLimit returns a TimeoutLimit. +func NewTimeoutLimit(n int) TimeoutLimit { + return TimeoutLimit{ + limit: NewLimit(n), + cond: NewCond(), + } +} + +// Borrow borrows with given timeout. +func (l TimeoutLimit) Borrow(timeout time.Duration) error { + if l.TryBorrow() { + return nil + } + + var ok bool + for { + timeout, ok = l.cond.WaitWithTimeout(timeout) + if ok && l.TryBorrow() { + return nil + } + + if timeout <= 0 { + return ErrTimeout + } + } +} + +// Return returns a borrow. +func (l TimeoutLimit) Return() error { + if err := l.limit.Return(); err != nil { + return err + } + + l.cond.Signal() + return nil +} + +// TryBorrow tries a borrow. +func (l TimeoutLimit) TryBorrow() bool { + return l.limit.TryBorrow() +} diff --git a/pkg/syncx/timeoutlimit_test.go b/pkg/syncx/timeoutlimit_test.go new file mode 100644 index 0000000..ffed96d --- /dev/null +++ b/pkg/syncx/timeoutlimit_test.go @@ -0,0 +1,52 @@ +package syncx + +import ( + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestTimeoutLimit(t *testing.T) { + tests := []struct { + name string + interval time.Duration + }{ + { + name: "no wait", + }, + { + name: "wait", + interval: time.Millisecond * 100, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + limit := NewTimeoutLimit(2) + assert.Nil(t, limit.Borrow(time.Millisecond*200)) + assert.Nil(t, limit.Borrow(time.Millisecond*200)) + var wait1, wait2, wait3 sync.WaitGroup + wait1.Add(1) + wait2.Add(1) + wait3.Add(1) + go func() { + wait1.Wait() + wait2.Done() + time.Sleep(test.interval) + assert.Nil(t, limit.Return()) + wait3.Done() + }() + wait1.Done() + wait2.Wait() + assert.Nil(t, limit.Borrow(time.Second)) + wait3.Wait() + assert.Equal(t, ErrTimeout, limit.Borrow(time.Millisecond*100)) + assert.Nil(t, limit.Return()) + assert.Nil(t, limit.Return()) + assert.Equal(t, ErrLimitReturn, limit.Return()) + }) + } +} diff --git a/pkg/templatex/render.go b/pkg/templatex/render.go new file mode 100644 index 0000000..968439f --- /dev/null +++ b/pkg/templatex/render.go @@ -0,0 +1,19 @@ +package templatex + +import ( + "bytes" + "text/template" +) + +func RenderToString(tmpl string, data map[string]interface{}) (string, error) { + t, err := template.New("tmpl").Parse(tmpl) + if err != nil { + return "", err + } + var buf bytes.Buffer + if err := t.Execute(&buf, data); err != nil { + return "", err + } + + return buf.String(), nil +} diff --git a/pkg/templatex/render_test.go b/pkg/templatex/render_test.go new file mode 100644 index 0000000..98e9c05 --- /dev/null +++ b/pkg/templatex/render_test.go @@ -0,0 +1,17 @@ +package templatex + +import "testing" + +func TestRenderToString(t *testing.T) { + tmpl := "hello {{.Name}}" + data := map[string]interface{}{ + "Name": "world", + } + got, err := RenderToString(tmpl, data) + if err != nil { + t.Fatalf("RenderToString() error = %v", err) + return + } + want := "hello world" + t.Logf("got: %v, want: %v", got, want) +} diff --git a/pkg/threading/routinegroup.go b/pkg/threading/routinegroup.go new file mode 100644 index 0000000..a6da73c --- /dev/null +++ b/pkg/threading/routinegroup.go @@ -0,0 +1,44 @@ +package threading + +import ( + "sync" +) + +// A RoutineGroup is used to group goroutines together and all wait all goroutines to be done. +type RoutineGroup struct { + waitGroup sync.WaitGroup +} + +// NewRoutineGroup returns a RoutineGroup. +func NewRoutineGroup() *RoutineGroup { + return new(RoutineGroup) +} + +// Run runs the given fn in RoutineGroup. +// Don't reference the variables from outside, +// because outside variables can be changed by other goroutines +func (g *RoutineGroup) Run(fn func()) { + g.waitGroup.Add(1) + + go func() { + defer g.waitGroup.Done() + fn() + }() +} + +// RunSafe runs the given fn in RoutineGroup, and avoid panics. +// Don't reference the variables from outside, +// because outside variables can be changed by other goroutines +func (g *RoutineGroup) RunSafe(fn func()) { + g.waitGroup.Add(1) + + GoSafe(func() { + defer g.waitGroup.Done() + fn() + }) +} + +// Wait waits all running functions to be done. +func (g *RoutineGroup) Wait() { + g.waitGroup.Wait() +} diff --git a/pkg/threading/routinegroup_test.go b/pkg/threading/routinegroup_test.go new file mode 100644 index 0000000..7f4dd08 --- /dev/null +++ b/pkg/threading/routinegroup_test.go @@ -0,0 +1,45 @@ +package threading + +import ( + "io" + "log" + "sync" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRoutineGroupRun(t *testing.T) { + var count int32 + group := NewRoutineGroup() + for i := 0; i < 3; i++ { + group.Run(func() { + atomic.AddInt32(&count, 1) + }) + } + + group.Wait() + + assert.Equal(t, int32(3), count) +} + +func TestRoutingGroupRunSafe(t *testing.T) { + log.SetOutput(io.Discard) + + var count int32 + group := NewRoutineGroup() + var once sync.Once + for i := 0; i < 3; i++ { + group.RunSafe(func() { + once.Do(func() { + panic("") + }) + atomic.AddInt32(&count, 1) + }) + } + + group.Wait() + + assert.Equal(t, int32(2), count) +} diff --git a/pkg/threading/routines.go b/pkg/threading/routines.go new file mode 100644 index 0000000..f7508a9 --- /dev/null +++ b/pkg/threading/routines.go @@ -0,0 +1,46 @@ +package threading + +import ( + "bytes" + "context" + "runtime" + "strconv" + + "github.com/perfect-panel/ppanel-server/pkg/rescue" +) + +// GoSafe runs the given fn using another goroutine, recovers if fn panics. +func GoSafe(fn func()) { + go RunSafe(fn) +} + +// GoSafeCtx runs the given fn using another goroutine, recovers if fn panics with ctx. +func GoSafeCtx(ctx context.Context, fn func()) { + go RunSafeCtx(ctx, fn) +} + +// RoutineId is only for debug, never use it in production. +func RoutineId() uint64 { + b := make([]byte, 64) + b = b[:runtime.Stack(b, false)] + b = bytes.TrimPrefix(b, []byte("goroutine ")) + b = b[:bytes.IndexByte(b, ' ')] + // if error, just return 0 + n, _ := strconv.ParseUint(string(b), 10, 64) + + return n +} + +// RunSafe runs the given fn, recovers if fn panics. +func RunSafe(fn func()) { + defer rescue.Recover() + + fn() +} + +// RunSafeCtx runs the given fn, recovers if fn panics with ctx. +func RunSafeCtx(ctx context.Context, fn func()) { + defer rescue.RecoverCtx(ctx) + + fn() +} diff --git a/pkg/threading/routines_test.go b/pkg/threading/routines_test.go new file mode 100644 index 0000000..dfc1d7c --- /dev/null +++ b/pkg/threading/routines_test.go @@ -0,0 +1,11 @@ +package threading + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRoutineId(t *testing.T) { + assert.True(t, RoutineId() > 0) +} diff --git a/pkg/timex/relativetime.go b/pkg/timex/relativetime.go new file mode 100644 index 0000000..23375a0 --- /dev/null +++ b/pkg/timex/relativetime.go @@ -0,0 +1,17 @@ +package timex + +import "time" + +// Use the long enough past time as start time, in case timex.Now() - lastTime equals 0. +var initTime = time.Now().AddDate(-1, -1, -1) + +// Now returns a relative time duration since initTime, which is not important. +// The caller only needs to care about the relative value. +func Now() time.Duration { + return time.Since(initTime) +} + +// Since returns a diff since given d. +func Since(d time.Duration) time.Duration { + return time.Since(initTime) - d +} diff --git a/pkg/timex/relativetime_test.go b/pkg/timex/relativetime_test.go new file mode 100644 index 0000000..950dbc6 --- /dev/null +++ b/pkg/timex/relativetime_test.go @@ -0,0 +1,32 @@ +package timex + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestRelativeTime(t *testing.T) { + time.Sleep(time.Millisecond) + now := Now() + assert.True(t, now > 0) + time.Sleep(time.Millisecond) + assert.True(t, Since(now) > 0) +} + +func BenchmarkTimeSince(b *testing.B) { + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + _ = time.Since(time.Now()) + } +} + +func BenchmarkTimexSince(b *testing.B) { + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + _ = Since(Now()) + } +} diff --git a/pkg/timex/repr.go b/pkg/timex/repr.go new file mode 100644 index 0000000..856eb96 --- /dev/null +++ b/pkg/timex/repr.go @@ -0,0 +1,11 @@ +package timex + +import ( + "fmt" + "time" +) + +// ReprOfDuration returns the string representation of given duration in ms. +func ReprOfDuration(duration time.Duration) string { + return fmt.Sprintf("%.1fms", float32(duration)/float32(time.Millisecond)) +} diff --git a/pkg/timex/repr_test.go b/pkg/timex/repr_test.go new file mode 100644 index 0000000..f787418 --- /dev/null +++ b/pkg/timex/repr_test.go @@ -0,0 +1,14 @@ +package timex + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestReprOfDuration(t *testing.T) { + assert.Equal(t, "1000.0ms", ReprOfDuration(time.Second)) + assert.Equal(t, "1111.6ms", ReprOfDuration( + time.Second+time.Millisecond*111+time.Microsecond*555)) +} diff --git a/pkg/timex/ticker.go b/pkg/timex/ticker.go new file mode 100644 index 0000000..c0ddf96 --- /dev/null +++ b/pkg/timex/ticker.go @@ -0,0 +1,80 @@ +package timex + +import ( + "errors" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/lang" +) + +// errTimeout indicates a timeout. +var errTimeout = errors.New("timeout") + +type ( + // Ticker interface wraps the Chan and Stop methods. + Ticker interface { + Chan() <-chan time.Time + Stop() + } + + // FakeTicker interface is used for unit testing. + FakeTicker interface { + Ticker + Done() + Tick() + Wait(d time.Duration) error + } + + fakeTicker struct { + c chan time.Time + done chan lang.PlaceholderType + } + + realTicker struct { + *time.Ticker + } +) + +// NewTicker returns a Ticker. +func NewTicker(d time.Duration) Ticker { + return &realTicker{ + Ticker: time.NewTicker(d), + } +} + +func (rt *realTicker) Chan() <-chan time.Time { + return rt.C +} + +// NewFakeTicker returns a FakeTicker. +func NewFakeTicker() FakeTicker { + return &fakeTicker{ + c: make(chan time.Time, 1), + done: make(chan lang.PlaceholderType, 1), + } +} + +func (ft *fakeTicker) Chan() <-chan time.Time { + return ft.c +} + +func (ft *fakeTicker) Done() { + ft.done <- lang.Placeholder +} + +func (ft *fakeTicker) Stop() { + close(ft.c) +} + +func (ft *fakeTicker) Tick() { + ft.c <- time.Now() +} + +func (ft *fakeTicker) Wait(d time.Duration) error { + select { + case <-time.After(d): + return errTimeout + case <-ft.done: + return nil + } +} diff --git a/pkg/timex/ticker_test.go b/pkg/timex/ticker_test.go new file mode 100644 index 0000000..66c0dce --- /dev/null +++ b/pkg/timex/ticker_test.go @@ -0,0 +1,50 @@ +package timex + +import ( + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestRealTickerDoTick(t *testing.T) { + ticker := NewTicker(time.Millisecond * 10) + defer ticker.Stop() + var count int + for range ticker.Chan() { + count++ + if count > 5 { + break + } + } +} + +func TestFakeTicker(t *testing.T) { + const total = 5 + ticker := NewFakeTicker() + defer ticker.Stop() + + var count int32 + go func() { + for range ticker.Chan() { + if atomic.AddInt32(&count, 1) == total { + ticker.Done() + } + } + }() + + for i := 0; i < 5; i++ { + ticker.Tick() + } + + assert.Nil(t, ticker.Wait(time.Second)) + assert.Equal(t, int32(total), atomic.LoadInt32(&count)) +} + +func TestFakeTickerTimeout(t *testing.T) { + ticker := NewFakeTicker() + defer ticker.Stop() + + assert.NotNil(t, ticker.Wait(time.Millisecond)) +} diff --git a/pkg/tool/base64.go b/pkg/tool/base64.go new file mode 100644 index 0000000..006928e --- /dev/null +++ b/pkg/tool/base64.go @@ -0,0 +1,42 @@ +package tool + +import ( + "encoding/base64" + "strings" +) + +// IsValidImageSize 检查base64图片是否有效且未超出大小限制 +// base64Str: base64编码的图片字符串 +// maxSizeKB: 最大允许大小(KB),int64类型 +// 返回: bool - true表示图片有效且未超限,false表示无效或超限 +func IsValidImageSize(base64Str string, maxSizeKB int64) bool { + // 输入验证 + if base64Str == "" || maxSizeKB < 0 { + return false + } + + // 提取纯base64数据 + data := base64Str + if idx := strings.Index(base64Str, ","); idx != -1 { + data = base64Str[idx+1:] + } + + // 快速估算大小(避免完整解码) + // base64编码后每3字节原始数据变成4字节,解码后大小约为输入长度的3/4 + approxSizeBytes := int64(len(data)) * 3 / 4 + approxSizeKB := approxSizeBytes / 1024 + + // 如果估算大小已超限,无需解码 + if approxSizeKB > maxSizeKB { + return false + } + + // 验证base64有效性 + decoded, err := base64.StdEncoding.DecodeString(data) + if err != nil { + return false + } + + // 精确检查大小 + return int64(len(decoded))/1024 <= maxSizeKB +} diff --git a/pkg/tool/base64_test.go b/pkg/tool/base64_test.go new file mode 100644 index 0000000..f4981b3 --- /dev/null +++ b/pkg/tool/base64_test.go @@ -0,0 +1,14 @@ +package tool + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsValidImageSize(t *testing.T) { + testBase64 := "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + maxSize := int64(10) + result := IsValidImageSize(testBase64, maxSize) + assert.Equal(t, result, true) +} diff --git a/pkg/tool/cipher.go b/pkg/tool/cipher.go new file mode 100644 index 0000000..221b113 --- /dev/null +++ b/pkg/tool/cipher.go @@ -0,0 +1,19 @@ +package tool + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" +) + +// GenerateCipher 根据公钥生成固定长度密文 +func GenerateCipher(serverKey string, length int) string { + h := hmac.New(sha256.New, []byte(serverKey)) + hash := h.Sum(nil) + hashStr := hex.EncodeToString(hash) + // Prevent overflow + if length > len(hashStr) { + length = len(hashStr) + } + return hashStr[:length] +} diff --git a/pkg/tool/cipher_test.go b/pkg/tool/cipher_test.go new file mode 100644 index 0000000..fb0f592 --- /dev/null +++ b/pkg/tool/cipher_test.go @@ -0,0 +1,10 @@ +package tool + +import ( + "testing" +) + +func TestGenerateCipher(t *testing.T) { + pwd := GenerateCipher("serverKey", 128) + t.Logf("pwd: %s", pwd) +} diff --git a/pkg/tool/convert.go b/pkg/tool/convert.go new file mode 100644 index 0000000..fa93055 --- /dev/null +++ b/pkg/tool/convert.go @@ -0,0 +1,32 @@ +package tool + +import ( + "fmt" + "reflect" + "strconv" +) + +// ConvertValueToString converts the value to string +func ConvertValueToString(value reflect.Value) string { + switch value.Kind() { + case reflect.String: + return value.String() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return strconv.FormatInt(value.Int(), 10) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return strconv.FormatUint(value.Uint(), 10) + case reflect.Float32, reflect.Float64: + return strconv.FormatFloat(value.Float(), 'f', -1, 64) + case reflect.Bool: + return strconv.FormatBool(value.Bool()) + case reflect.Ptr: + switch value.Type().Elem().Kind() { + case reflect.Bool: + return fmt.Sprintf("%v", value.Elem().Bool()) + default: + return "" + } + default: + return "" + } +} diff --git a/pkg/tool/copy.go b/pkg/tool/copy.go new file mode 100644 index 0000000..6464426 --- /dev/null +++ b/pkg/tool/copy.go @@ -0,0 +1,87 @@ +package tool + +import ( + "encoding/json" + "fmt" + "reflect" + "strconv" + "time" + + "github.com/jinzhu/copier" + "github.com/pkg/errors" + + "github.com/perfect-panel/ppanel-server/pkg/constant" +) + +func DeepCopy[T, K interface{}](destStruct T, srcStruct K) T { + var dst = destStruct + var src = srcStruct + _ = copier.CopyWithOption(dst, src, copier.Option{ + DeepCopy: true, + IgnoreEmpty: true, + Converters: []copier.TypeConverter{ + { + SrcType: time.Time{}, + DstType: constant.Int64, + Fn: func(src interface{}) (interface{}, error) { + s, ok := src.(time.Time) + if !ok { + return nil, errors.New("src type not matching") + } + return s.UnixMilli(), nil + }, + }, + }, + }) + return dst +} +func ShallowCopy[T, K interface{}](destStruct T, srcStruct K) T { + var dst = destStruct + var src = srcStruct + _ = copier.CopyWithOption(dst, src, copier.Option{ + IgnoreEmpty: true, + Converters: []copier.TypeConverter{ + { + SrcType: time.Time{}, + DstType: constant.Int64, + Fn: func(src interface{}) (interface{}, error) { + s, ok := src.(time.Time) + + if !ok { + return nil, errors.New("src type not matching") + } + return s.UnixMilli(), nil + }, + }, + }, + }) + return dst +} + +func Int64ToStringSlice(intSlice []int64) []string { + strSlice := make([]string, len(intSlice)) + for i, n := range intSlice { + strSlice[i] = strconv.FormatInt(n, 10) + } + return strSlice +} + +func CloneMapToStruct(input any, output interface{}) error { + // 确保 output 是一个指针,并且指向一个结构体 + val := reflect.ValueOf(output) + if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Struct { + return fmt.Errorf("output must be a pointer to a struct") + } + + // 使用 JSON 编解码将 map 转换为结构体 + data, err := json.Marshal(input) + if err != nil { + return fmt.Errorf("failed to marshal input: %w", err) + } + + err = json.Unmarshal(data, output) + if err != nil { + return fmt.Errorf("failed to unmarshal data to struct: %w", err) + } + return nil +} diff --git a/pkg/tool/curve25519.go b/pkg/tool/curve25519.go new file mode 100644 index 0000000..c3312ba --- /dev/null +++ b/pkg/tool/curve25519.go @@ -0,0 +1,51 @@ +package tool + +import ( + "crypto/rand" + "encoding/base64" + + "github.com/pkg/errors" + + "golang.org/x/crypto/curve25519" +) + +func Curve25519Genkey(StdEncoding bool, inputBase64 string) (public, private string, err error) { + var privateKey, publicKey []byte + var encoding *base64.Encoding + if StdEncoding { + encoding = base64.StdEncoding + } else { + encoding = base64.RawURLEncoding + } + + if len(inputBase64) > 0 { + privateKey, err = encoding.DecodeString(inputBase64) + if err != nil { + goto out + } + if len(privateKey) != curve25519.ScalarSize { + err = errors.New("Invalid length of private key.") + goto out + } + } + + if privateKey == nil { + privateKey = make([]byte, curve25519.ScalarSize) + if _, err = rand.Read(privateKey); err != nil { + goto out + } + } + + // Modify random bytes using algorithm described at: + // https://cr.yp.to/ecdh.html. + privateKey[0] &= 248 + privateKey[31] &= 127 | 64 + + if publicKey, err = curve25519.X25519(privateKey, curve25519.Basepoint); err != nil { + goto out + } + public = encoding.EncodeToString(publicKey) + private = encoding.EncodeToString(privateKey) +out: + return public, private, err +} diff --git a/pkg/tool/email.go b/pkg/tool/email.go new file mode 100644 index 0000000..0063e3b --- /dev/null +++ b/pkg/tool/email.go @@ -0,0 +1,23 @@ +package tool + +import ( + "strings" +) + +func MaskEmail(email string) string { + atIndex := strings.Index(email, "@") + if atIndex == -1 || atIndex == 0 || atIndex == len(email)-1 { + return email + } + localPart := email[:atIndex] + domainPart := email[atIndex+1:] + + // 本地部分需要至少保留首字符和末字符 + if len(localPart) < 2 { + return email + } + // 替换本地部分中间字符为星号 + maskedLocal := string(localPart[0]) + strings.Repeat("*", len(localPart)-2) + string(localPart[len(localPart)-1]) + // 返回处理后的邮箱地址 + return maskedLocal + "@" + domainPart +} diff --git a/pkg/tool/encryption.go b/pkg/tool/encryption.go new file mode 100644 index 0000000..e4f205e --- /dev/null +++ b/pkg/tool/encryption.go @@ -0,0 +1,34 @@ +package tool + +import ( + "crypto/md5" + "crypto/sha512" + "encoding/hex" + "fmt" + "strings" + + "github.com/anaskhan96/go-password-encoder" +) + +var options = &password.Options{SaltLen: 16, Iterations: 100, KeyLen: 32, HashFunction: sha512.New} + +func EncodePassWord(str string) string { + salt, encodedPwd := password.Encode(str, options) + newPassword := fmt.Sprintf("$pbkdf2-sha512$%s$%s", salt, encodedPwd) + return newPassword +} + +func VerifyPassWord(passwd, EncodePasswd string) bool { + info := strings.Split(EncodePasswd, "$") + return password.Verify(passwd, info[2], info[3], options) +} + +func Md5Encode(str string, isUpper bool) string { + sum := md5.Sum([]byte(str)) + res := hex.EncodeToString(sum[:]) + //转大写,strings.ToUpper(res) + if isUpper { + res = strings.ToUpper(res) + } + return res +} diff --git a/pkg/tool/encryption_test.go b/pkg/tool/encryption_test.go new file mode 100644 index 0000000..45e0a18 --- /dev/null +++ b/pkg/tool/encryption_test.go @@ -0,0 +1,7 @@ +package tool + +import "testing" + +func TestEncodePassWord(t *testing.T) { + t.Logf("EncodePassWord: %v", EncodePassWord("")) +} diff --git a/pkg/tool/etag.go b/pkg/tool/etag.go new file mode 100644 index 0000000..53bb2f6 --- /dev/null +++ b/pkg/tool/etag.go @@ -0,0 +1,11 @@ +package tool + +import ( + "crypto/sha256" + "encoding/hex" +) + +func GenerateETag(data []byte) string { + hash := sha256.Sum256(data) + return hex.EncodeToString(hash[:]) +} diff --git a/pkg/tool/format_float_test.go b/pkg/tool/format_float_test.go new file mode 100644 index 0000000..4310412 --- /dev/null +++ b/pkg/tool/format_float_test.go @@ -0,0 +1,10 @@ +package tool + +import "testing" + +func TestFormatStringToFloat(t *testing.T) { + var value = 1.23 + if FormatStringToFloat("1.23") != value { + t.Errorf("Expected %f, but got %f", value, FormatStringToFloat("1.23")) + } +} diff --git a/pkg/tool/fromatFloat.go b/pkg/tool/fromatFloat.go new file mode 100644 index 0000000..f843874 --- /dev/null +++ b/pkg/tool/fromatFloat.go @@ -0,0 +1,26 @@ +package tool + +import ( + "math" + "strconv" +) + +func FormatFloat(num float64, decimal int) string { + // 默认乘1 + d := float64(1) + if decimal > 0 { + // 10的N次方 + d = math.Pow10(decimal) + } + // math.trunc作用就是返回浮点数的整数部分 + // 再除回去,小数点后无效的0也就不存在了 + return strconv.FormatFloat(math.Trunc(num*d)/d, 'f', -1, 64) +} + +func FormatStringToFloat(str string) float64 { + value, err := strconv.ParseFloat(str, 64) + if err != nil { + return 0 + } + return value +} diff --git a/pkg/tool/redis.go b/pkg/tool/redis.go new file mode 100644 index 0000000..87839ee --- /dev/null +++ b/pkg/tool/redis.go @@ -0,0 +1,45 @@ +package tool + +import ( + "context" + "fmt" + "net/url" + + "github.com/redis/go-redis/v9" +) + +func ParseRedisURI(uri string) (addr, password string, database int, err error) { + parsedURI, err := url.Parse(uri) + + if err != nil { + return "", "", 0, err + } + host := parsedURI.Hostname() + port := parsedURI.Port() + if port == "" { + port = "6379" + } + addr = fmt.Sprintf("%s:%s", host, port) + + // password + if parsedURI.User != nil { + password, _ = parsedURI.User.Password() + } + if len(parsedURI.Path) > 1 { // Path: "/0" + var dbIndex int + _, err = fmt.Sscanf(parsedURI.Path, "/%d", &dbIndex) + if err == nil { + database = dbIndex + } + } + return +} + +func RedisPing(addr, password string, database int) error { + rds := redis.NewClient(&redis.Options{ + Addr: addr, + Password: password, + DB: database, + }) + return rds.Ping(context.Background()).Err() +} diff --git a/pkg/tool/redis_test.go b/pkg/tool/redis_test.go new file mode 100644 index 0000000..d57c294 --- /dev/null +++ b/pkg/tool/redis_test.go @@ -0,0 +1,24 @@ +package tool + +import "testing" + +func TestParseRedisURI(t *testing.T) { + uri := "redis://localhost:6379" + addr, password, database, err := ParseRedisURI(uri) + if err != nil { + t.Fatal(err) + } + t.Log(addr, password, database) +} + +func TestRedisPing(t *testing.T) { + uri := "redis://localhost:6379" + addr, password, database, err := ParseRedisURI(uri) + if err != nil { + t.Fatal(err) + } + err = RedisPing(addr, password, database) + if err != nil { + t.Fatal(err) + } +} diff --git a/pkg/tool/setProcessing.go b/pkg/tool/setProcessing.go new file mode 100644 index 0000000..8934e05 --- /dev/null +++ b/pkg/tool/setProcessing.go @@ -0,0 +1,45 @@ +package tool + +func SliceIntersectInt64(slice1, slice2 []int64) []int64 { + m := make(map[int64]int) + nn := make([]int64, 0) + for _, v := range slice1 { + m[v]++ + } + + for _, v := range slice2 { + times := m[v] + if times == 1 { + nn = append(nn, v) + } + } + return nn +} + +// SliceDifferenceInt64 returns the difference of two slices +func SliceDifferenceInt64(slice1, slice2 []int64) []int64 { + m := make(map[int64]int) + nn := make([]int64, 0) + inter := SliceIntersectInt64(slice1, slice2) + for _, v := range inter { + m[v]++ + } + + for _, value := range slice1 { + times := m[value] + if times == 0 { + nn = append(nn, value) + } + } + return nn +} + +// SliceIsExistInt64 checks if a value exists in a slice +func SliceIsExistInt64(slice []int64, value int64) bool { + for _, item := range slice { + if item == value { + return true + } + } + return false +} diff --git a/pkg/tool/sha.go b/pkg/tool/sha.go new file mode 100644 index 0000000..779c224 --- /dev/null +++ b/pkg/tool/sha.go @@ -0,0 +1,14 @@ +package tool + +import ( + "crypto/sha1" + "fmt" +) + +func GenerateShortID(privateKey string) string { + hash := sha1.New() + hash.Write([]byte(privateKey)) + hashValue := hash.Sum(nil) + hashString := fmt.Sprintf("%x", hashValue) + return hashString[:8] +} diff --git a/pkg/tool/slice.go b/pkg/tool/slice.go new file mode 100644 index 0000000..337b357 --- /dev/null +++ b/pkg/tool/slice.go @@ -0,0 +1,130 @@ +package tool + +import ( + "fmt" + "strconv" + "strings" + + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +func Int64SliceToStringSlice(slice []int64) []string { + stringSlice := make([]string, len(slice)) + for i, num := range slice { + stringSlice[i] = strconv.Itoa(int(num)) + } + return stringSlice +} +func StringSliceToInt64Slice(slice []string) []int64 { + int64Slice := make([]int64, len(slice)) + for i, str := range slice { + num, err := strconv.Atoi(str) + if err != nil { + fmt.Println("Error converting string to int:", err) + continue + } + int64Slice[i] = int64(num) + } + return int64Slice +} + +func StringToInt64Slice(s string) []int64 { + + stringSlice := strings.Split(s, ",") + var intSlice []int64 + if len(s) == 0 { + return intSlice + } + for _, str := range stringSlice { + num, err := strconv.ParseInt(strings.TrimSpace(str), 10, 64) + if err != nil { + logger.Error("[Tools] StringToInt64Slice", + logger.Field("error", err.Error()), + logger.Field("str", str), + ) + continue + } + intSlice = append(intSlice, num) + } + return intSlice +} + +func Int64SliceToString(intSlice []int64) string { + var strSlice []string + for _, num := range intSlice { + strSlice = append(strSlice, strconv.FormatInt(num, 10)) + } + return strings.Join(strSlice, ",") +} + +// string slice to string +func StringSliceToString(stringSlice []string) string { + return strings.Join(stringSlice, ",") +} + +// StringMergeAndRemoveDuplicates Tool function to convert multiple comma separated strings into [] strings and deduplicate them +func StringMergeAndRemoveDuplicates(strs ...string) []string { + if len(strs) == 1 && strs[0] == "" { + return []string{} + } + merged := make([]string, 0) + for _, str := range strs { + merged = append(merged, strings.Split(str, ",")...) + } + uniqueMap := make(map[string]bool) + var uniqueList []string + + for _, item := range merged { + if !uniqueMap[item] { + uniqueMap[item] = true + uniqueList = append(uniqueList, item) + } + } + + return uniqueList +} + +func StringSliceContains(slice []string, str string) bool { + for _, s := range slice { + if s == str { + return true + } + } + return false +} + +func RemoveElementBySlice[T comparable](slice []T, element T) []T { + result := make([]T, 0, len(slice)) + for _, s := range slice { + if s != element { + result = append(result, s) + } + } + return result +} + +func RemoveDuplicateElements[T comparable](input ...T) []T { + uniqueMap := make(map[T]struct{}) + var result []T + + for _, item := range input { + // 仅在 T 是 string 类型时跳过空字符串 + if v, ok := any(item).(string); ok && v == "" { + continue + } + if _, exists := uniqueMap[item]; !exists { + uniqueMap[item] = struct{}{} + result = append(result, item) + } + } + return result +} + +func Contains[T comparable](slice []T, target T) bool { + for _, v := range slice { + if v == target { + return true + } + } + return false +} diff --git a/pkg/tool/sliceReflectToStruct.go b/pkg/tool/sliceReflectToStruct.go new file mode 100644 index 0000000..644e91b --- /dev/null +++ b/pkg/tool/sliceReflectToStruct.go @@ -0,0 +1,46 @@ +package tool + +import ( + "reflect" + "strconv" + + "github.com/goccy/go-json" + "github.com/perfect-panel/ppanel-server/internal/model/system" +) + +func SystemConfigSliceReflectToStruct(slice []*system.System, structType any) { + v := reflect.ValueOf(structType).Elem() + + for _, config := range slice { + field := v.FieldByName(config.Key) + if field.Kind() == reflect.Ptr && field.Type().Elem().Kind() == reflect.Bool { + if config.Value == "" { + field.Set(reflect.Zero(field.Type())) + } else { + boolValue, _ := strconv.ParseBool(config.Value) + field.Set(reflect.ValueOf(&boolValue)) + } + continue + } + + if field.IsValid() && field.CanSet() { + switch config.Type { + case "string": + field.SetString(config.Value) + case "bool": + boolValue, _ := strconv.ParseBool(config.Value) + field.SetBool(boolValue) + case "int": + intValue, _ := strconv.Atoi(config.Value) + field.SetInt(int64(intValue)) + case "int64": + intValue, _ := strconv.ParseInt(config.Value, 10, 64) + field.SetInt(intValue) + case "interface": + _ = json.Unmarshal([]byte(config.Value), field.Addr().Interface()) + default: + break + } + } + } +} diff --git a/pkg/tool/template.go b/pkg/tool/template.go new file mode 100644 index 0000000..f2e0c6d --- /dev/null +++ b/pkg/tool/template.go @@ -0,0 +1,26 @@ +package tool + +import ( + "bytes" + "text/template" +) + +func RenderTemplateToString(tmpl string, data interface{}) (string, error) { + // 解析模板 + t, err := template.New("template").Parse(tmpl) + if err != nil { + return "", err + } + + // 创建缓冲区存储结果 + var buf bytes.Buffer + + // 执行模板 + err = t.Execute(&buf, data) + if err != nil { + return "", err + } + + // 返回结果字符串 + return buf.String(), nil +} diff --git a/pkg/tool/tern.go b/pkg/tool/tern.go new file mode 100644 index 0000000..e9f1132 --- /dev/null +++ b/pkg/tool/tern.go @@ -0,0 +1,9 @@ +package tool + +// Tern 三目运算 +func Tern[T any](cond bool, a, b T) T { + if cond { + return a + } + return b +} diff --git a/pkg/tool/time.go b/pkg/tool/time.go new file mode 100644 index 0000000..67a2535 --- /dev/null +++ b/pkg/tool/time.go @@ -0,0 +1,140 @@ +package tool + +import ( + "time" + + "github.com/perfect-panel/ppanel-server/pkg/logger" +) + +func AddTime(unit string, quantity int64, baseTime ...time.Time) time.Time { + basic := time.Now() + if len(baseTime) != 0 { + basic = baseTime[0] + } + switch unit { + case "Year": + return basic.AddDate(int(quantity), 0, 0) + case "Month": + return basic.AddDate(0, int(quantity), 0) + case "Day": + return basic.AddDate(0, 0, int(quantity)) + case "Hour": + return basic.Add(time.Hour * time.Duration(quantity)) + case "Minute": + return basic.Add(time.Minute * time.Duration(quantity)) + case "NoLimit": + return time.UnixMilli(0) + default: + logger.Error("[Tool] Unknown time unit", logger.Field("unit", unit)) + return basic + } +} + +func MonthDiff(startTime, endTime time.Time) int { + startYear, startMonth, startDay := startTime.Year(), int(startTime.Month()), startTime.Day() + endYear, endMonth, endDay := endTime.Year(), int(endTime.Month()), endTime.Day() + + // 计算初步月份差 + monthDiff := (endYear-startYear)*12 + (endMonth - startMonth) + + // 检查是否要扣除不足一个完整月的部分 + if endDay <= startDay { + monthDiff-- // 如果结束时间的日期小于开始时间的日期,则不计为完整自然月 + } + + return monthDiff +} +func DaysToMonthDay(t time.Time, targetDay int) int64 { + currentDay := t.Day() + year, month := t.Year(), t.Month() + + var targetDate time.Time + + // 如果当前号数大于目标号数,计算本月目标号数 + if currentDay > targetDay { + targetDate = getValidDate(year, month, targetDay, t.Location()) + } else { // 如果当前号数小于等于目标号数,计算上个月目标号数 + if month == time.January { + year-- + month = time.December + } else { + month-- + } + targetDate = getValidDate(year, month, targetDay, t.Location()) + } + + // 计算时间差 + duration := t.Sub(targetDate) + return int64(duration.Hours() / 24) // 转换为整天数 +} + +func DaysToNextMonth(t time.Time) int64 { + // 获取下个月的1号 + year, month := t.Year(), t.Month() + if month == 12 { + year++ + month = 1 + } else { + month++ + } + nextMonthFirstDay := time.Date(year, month, 1, 0, 0, 0, 0, t.Location()) + + // 计算时间差 + duration := nextMonthFirstDay.Sub(t) + return int64(duration.Hours() / 24) // 转换为整天数 +} + +func getValidDate(year int, month time.Month, day int, loc *time.Location) time.Time { + // 构造当月的 1 号 + firstOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, loc) + + // 获取当月的天数 + lastDayOfMonth := firstOfMonth.AddDate(0, 1, -1).Day() + + // 如果目标号数超过当月的天数,则使用最后一天 + if day > lastDayOfMonth { + day = lastDayOfMonth + } + + return time.Date(year, month, day, 0, 0, 0, 0, loc) +} + +// GetLastDayOfMonth 获取指定时间所在月份的最后一天 +func GetLastDayOfMonth(t time.Time) int64 { + // 获取当前月份的第一天 + firstDayOfMonth := time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location()) + // 获取下个月的第一天,然后减去一天 + lastDayOfMonth := firstDayOfMonth.AddDate(0, 1, 0).Add(-time.Hour * 24) + return int64(lastDayOfMonth.Day()) +} + +func GetYearDays(t time.Time, month int, day int) int64 { + startTime := time.Date(t.Year(), time.Month(month), day, 0, 0, 0, 0, t.Location()) + endTime := time.Date(t.Year(), time.Month(month), day, 0, 0, 0, 0, t.Location()) + if endTime.After(t) { + startTime = time.Date(t.Year()-1, time.Month(month), day, 0, 0, 0, 0, t.Location()) + } else { + endTime = time.Date(t.Year()+1, time.Month(month), day, 0, 0, 0, 0, t.Location()) + } + return int64(endTime.Sub(startTime).Hours() / 24) +} + +func DaysToYearDay(t time.Time, month int, day int) int64 { + targetTime := time.Date(t.Year(), time.Month(month), day, 0, 0, 0, 0, t.Location()) + if targetTime.Before(t) { + targetTime = time.Date(t.Year()+1, time.Month(month), day, 0, 0, 0, 0, t.Location()) + } + + return int64(t.Sub(targetTime).Hours() / 24) +} + +func YearDiff(startTime, endTime time.Time) int { + // 计算基础年份差 + yearDiff := endTime.Year() - startTime.Year() + + // 检查结束时间是否在开始时间之前(同一年的同一天之前) + if endTime.Month() < startTime.Month() || (endTime.Month() == startTime.Month() && endTime.Day() < startTime.Day()) { + yearDiff-- // 不足一年则减去一年 + } + return yearDiff +} diff --git a/pkg/tool/time_test.go b/pkg/tool/time_test.go new file mode 100644 index 0000000..0da71d8 --- /dev/null +++ b/pkg/tool/time_test.go @@ -0,0 +1,19 @@ +package tool + +import ( + "testing" + "time" +) + +func TestAddTime(t *testing.T) { + basic := time.Now() + expAt := AddTime("Month", 1, basic) + + t.Logf("AddTime() success, expected year %d, got year %d, full: %v", basic.Year()+1, expAt.Year(), expAt.Format("2006-01-02 15:04:05")) +} + +func TestGetYearDays(t *testing.T) { + days := GetYearDays(time.Now(), 2, 1) + t.Logf("GetYearDays() success, expected 365, got %d", days) + +} diff --git a/pkg/tool/tradeNo.go b/pkg/tool/tradeNo.go new file mode 100644 index 0000000..20352ca --- /dev/null +++ b/pkg/tool/tradeNo.go @@ -0,0 +1,24 @@ +package tool + +import ( + "fmt" + "math/rand" + "strconv" + "strings" + "time" +) + +func GenerateTradeNo() string { + now := time.Now() + formattedTime := now.Format("20060102150405") + strconv.Itoa(now.Nanosecond()) + numeric := [10]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} + r := len(numeric) + source := rand.NewSource(time.Now().UnixNano()) + random := rand.New(source) + var code strings.Builder + for i := 0; i < 4; i++ { + _, _ = fmt.Fprintf(&code, "%d", numeric[random.Intn(r)]) + } + formattedTime += code.String() + return formattedTime +} diff --git a/pkg/tool/uilts.go b/pkg/tool/uilts.go new file mode 100644 index 0000000..e11e035 --- /dev/null +++ b/pkg/tool/uilts.go @@ -0,0 +1,10 @@ +package tool + +import ( + "fmt" + "time" +) + +func MicrosecondsStr(elapsed time.Duration) string { + return fmt.Sprintf("%.3fms", float64(elapsed.Nanoseconds())/1e6) +} diff --git a/pkg/tool/version.go b/pkg/tool/version.go new file mode 100644 index 0000000..7c1ddbb --- /dev/null +++ b/pkg/tool/version.go @@ -0,0 +1,21 @@ +package tool + +import ( + "regexp" + "strconv" +) + +func ExtractVersionNumber(versionStr string) int { + // 正则表达式匹配括号中的数字 eg. 1.0.0(10000) + re := regexp.MustCompile(`\((\d+)\)`) + matches := re.FindStringSubmatch(versionStr) + + if len(matches) > 1 { + // 转换成数字 + num, err := strconv.Atoi(matches[1]) + if err == nil { + return num + } + } + return 0 +} diff --git a/pkg/tool/version_test.go b/pkg/tool/version_test.go new file mode 100644 index 0000000..2878371 --- /dev/null +++ b/pkg/tool/version_test.go @@ -0,0 +1,12 @@ +package tool + +import ( + "testing" + + "github.com/perfect-panel/ppanel-server/pkg/constant" +) + +func TestExtractVersionNumber(t *testing.T) { + versionNumber := ExtractVersionNumber(constant.Version) + t.Log(versionNumber) +} diff --git a/pkg/trace/agent.go b/pkg/trace/agent.go new file mode 100644 index 0000000..c66f8c4 --- /dev/null +++ b/pkg/trace/agent.go @@ -0,0 +1,155 @@ +package trace + +//nolint:staticcheck +import ( + "context" + "fmt" + "net/url" + "os" + "sync" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/jaeger" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + "go.opentelemetry.io/otel/exporters/zipkin" + "go.opentelemetry.io/otel/sdk/resource" + + sdktrace "go.opentelemetry.io/otel/sdk/trace" + + "github.com/perfect-panel/ppanel-server/pkg/lang" + "github.com/perfect-panel/ppanel-server/pkg/logger" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" +) + +const ( + kindJaeger = "jaeger" + kindZipkin = "zipkin" + kindOtlpGrpc = "otlpgrpc" + kindOtlpHttp = "otlphttp" + kindFile = "file" + protocolUdp = "udp" +) + +var ( + agents = make(map[string]lang.PlaceholderType) + lock sync.Mutex + tp *sdktrace.TracerProvider +) + +// StartAgent starts an opentelemetry agent. +func StartAgent(c Config) { + if c.Disabled { + return + } + logger.Info("Starting agent") + lock.Lock() + defer lock.Unlock() + + _, ok := agents[c.Endpoint] + if ok { + return + } + + // if error happens, let later calls run. + if err := startAgent(c); err != nil { + return + } + + agents[c.Endpoint] = lang.Placeholder +} + +// StopAgent shuts down the span processors in the order they were registered. +func StopAgent() { + lock.Lock() + defer lock.Unlock() + + if tp != nil { + _ = tp.Shutdown(context.Background()) + tp = nil + } +} + +func createExporter(c Config) (sdktrace.SpanExporter, error) { + // Just support jaeger and zipkin now, more for later + switch c.Batcher { + case kindJaeger: + u, err := url.Parse(c.Endpoint) + if err == nil && u.Scheme == protocolUdp { + return jaeger.New(jaeger.WithAgentEndpoint(jaeger.WithAgentHost(u.Hostname()), + jaeger.WithAgentPort(u.Port()))) + } + return jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(c.Endpoint))) + case kindZipkin: + return zipkin.New(c.Endpoint) + case kindOtlpGrpc: + // Always treat trace exporter as optional component, so we use nonblock here, + // otherwise this would slow down app start up even set a dial timeout here when + // endpoint can not reach. + // If the connection not dial success, the global otel ErrorHandler will catch error + // when reporting data like other exporters. + opts := []otlptracegrpc.Option{ + otlptracegrpc.WithInsecure(), + otlptracegrpc.WithEndpoint(c.Endpoint), + } + if len(c.OtlpHeaders) > 0 { + opts = append(opts, otlptracegrpc.WithHeaders(c.OtlpHeaders)) + } + return otlptracegrpc.New(context.Background(), opts...) + case kindOtlpHttp: + // Not support flexible configuration now. + opts := []otlptracehttp.Option{ + otlptracehttp.WithEndpoint(c.Endpoint), + } + + if !c.OtlpHttpSecure { + opts = append(opts, otlptracehttp.WithInsecure()) + } + if len(c.OtlpHeaders) > 0 { + opts = append(opts, otlptracehttp.WithHeaders(c.OtlpHeaders)) + } + if len(c.OtlpHttpPath) > 0 { + opts = append(opts, otlptracehttp.WithURLPath(c.OtlpHttpPath)) + } + return otlptracehttp.New(context.Background(), opts...) + case kindFile: + f, err := os.OpenFile(c.Endpoint, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + return nil, fmt.Errorf("file exporter endpoint error: %s", err.Error()) + } + return stdouttrace.New(stdouttrace.WithWriter(f)) + default: + return nil, fmt.Errorf("unknown exporter: %s", c.Batcher) + } +} + +func startAgent(c Config) error { + AddResources(semconv.ServiceNameKey.String(c.Name)) + + opts := []sdktrace.TracerProviderOption{ + // Set the sampling rate based on the parent span to 100% + sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(c.Sampler))), + // Record information about this application in a Resource. + sdktrace.WithResource(resource.NewSchemaless(attrResources...)), + } + + if len(c.Endpoint) > 0 { + exp, err := createExporter(c) + if err != nil { + logger.Error(err) + return err + } + + // Always be sure to batch in production. + opts = append(opts, sdktrace.WithBatcher(exp)) + } + + tp = sdktrace.NewTracerProvider(opts...) + otel.SetTracerProvider(tp) + otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) { + logger.Errorf("[otel] error: %v", err) + })) + + return nil +} diff --git a/pkg/trace/agent_test.go b/pkg/trace/agent_test.go new file mode 100644 index 0000000..7176419 --- /dev/null +++ b/pkg/trace/agent_test.go @@ -0,0 +1,111 @@ +package trace + +import ( + "testing" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/stretchr/testify/assert" +) + +func TestStartAgent(t *testing.T) { + logger.Disable() + + const ( + endpoint1 = "localhost:1234" + endpoint2 = "remotehost:1234" + endpoint3 = "localhost:1235" + endpoint4 = "localhost:1236" + endpoint5 = "udp://localhost:6831" + endpoint6 = "localhost:1237" + endpoint71 = "/tmp/trace.log" + endpoint72 = "/not-exist-fs/trace.log" + ) + c1 := Config{ + Name: "foo", + } + c2 := Config{ + Name: "bar", + Endpoint: endpoint1, + Batcher: kindJaeger, + } + c3 := Config{ + Name: "any", + Endpoint: endpoint2, + Batcher: kindZipkin, + } + c4 := Config{ + Name: "bla", + Endpoint: endpoint3, + Batcher: "otlp", + } + c5 := Config{ + Name: "otlpgrpc", + Endpoint: endpoint3, + Batcher: kindOtlpGrpc, + OtlpHeaders: map[string]string{ + "uptrace-dsn": "http://project2_secret_token@localhost:14317/2", + }, + } + c6 := Config{ + Name: "otlphttp", + Endpoint: endpoint4, + Batcher: kindOtlpHttp, + OtlpHeaders: map[string]string{ + "uptrace-dsn": "http://project2_secret_token@localhost:14318/2", + }, + OtlpHttpPath: "/v1/traces", + } + c7 := Config{ + Name: "UDP", + Endpoint: endpoint5, + Batcher: kindJaeger, + } + c8 := Config{ + Disabled: true, + Endpoint: endpoint6, + Batcher: kindJaeger, + } + c9 := Config{ + Name: "file", + Endpoint: endpoint71, + Batcher: kindFile, + } + c10 := Config{ + Name: "file", + Endpoint: endpoint72, + Batcher: kindFile, + } + + StartAgent(c1) + StartAgent(c1) + StartAgent(c2) + StartAgent(c3) + StartAgent(c4) + StartAgent(c5) + StartAgent(c6) + StartAgent(c7) + StartAgent(c8) + StartAgent(c9) + StartAgent(c10) + defer StopAgent() + + lock.Lock() + defer lock.Unlock() + + // because remotehost cannot be resolved + assert.Equal(t, 6, len(agents)) + _, ok := agents[""] + assert.True(t, ok) + _, ok = agents[endpoint1] + assert.True(t, ok) + _, ok = agents[endpoint2] + assert.False(t, ok) + _, ok = agents[endpoint5] + assert.True(t, ok) + _, ok = agents[endpoint6] + assert.False(t, ok) + _, ok = agents[endpoint71] + assert.True(t, ok) + _, ok = agents[endpoint72] + assert.False(t, ok) +} diff --git a/pkg/trace/attributes.go b/pkg/trace/attributes.go new file mode 100644 index 0000000..6da2e68 --- /dev/null +++ b/pkg/trace/attributes.go @@ -0,0 +1,40 @@ +package trace + +import ( + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" + gcodes "google.golang.org/grpc/codes" +) + +const ( + // GRPCStatusCodeKey is convention for numeric status code of a gRPC request. + GRPCStatusCodeKey = attribute.Key("rpc.grpc.status_code") + // RPCNameKey is the name of message transmitted or received. + RPCNameKey = attribute.Key("name") + // RPCMessageTypeKey is the type of message transmitted or received. + RPCMessageTypeKey = attribute.Key("message.type") + // RPCMessageIDKey is the identifier of message transmitted or received. + RPCMessageIDKey = attribute.Key("message.id") + // RPCMessageCompressedSizeKey is the compressed size of the message transmitted or received in bytes. + RPCMessageCompressedSizeKey = attribute.Key("message.compressed_size") + // RPCMessageUncompressedSizeKey is the uncompressed size of the message + // transmitted or received in bytes. + RPCMessageUncompressedSizeKey = attribute.Key("message.uncompressed_size") +) + +// Semantic conventions for common RPC attributes. +var ( + // RPCSystemGRPC is the semantic convention for gRPC as the remoting system. + RPCSystemGRPC = semconv.RPCSystemKey.String("grpc") + // RPCNameMessage is the semantic convention for a message named message. + RPCNameMessage = RPCNameKey.String("message") + // RPCMessageTypeSent is the semantic conventions for sent RPC message types. + RPCMessageTypeSent = RPCMessageTypeKey.String("SENT") + // RPCMessageTypeReceived is the semantic conventions for the received RPC message types. + RPCMessageTypeReceived = RPCMessageTypeKey.String("RECEIVED") +) + +// StatusCodeAttr returns an attribute.KeyValue that represents the give c. +func StatusCodeAttr(c gcodes.Code) attribute.KeyValue { + return GRPCStatusCodeKey.Int64(int64(c)) +} diff --git a/pkg/trace/attributes_test.go b/pkg/trace/attributes_test.go new file mode 100644 index 0000000..67c0097 --- /dev/null +++ b/pkg/trace/attributes_test.go @@ -0,0 +1,12 @@ +package trace + +import ( + "testing" + + "github.com/stretchr/testify/assert" + gcodes "google.golang.org/grpc/codes" +) + +func TestStatusCodeAttr(t *testing.T) { + assert.Equal(t, GRPCStatusCodeKey.Int(int(gcodes.DataLoss)), StatusCodeAttr(gcodes.DataLoss)) +} diff --git a/pkg/trace/config.go b/pkg/trace/config.go new file mode 100644 index 0000000..c668599 --- /dev/null +++ b/pkg/trace/config.go @@ -0,0 +1,24 @@ +package trace + +// TraceName represents the tracing name. +const TraceName = "ppanel" + +// A Config is an opentelemetry config. +type Config struct { + Name string `yaml:"Name"` + Endpoint string `yaml:"Endpoint"` + Sampler float64 `yaml:"Sampler" default:"1.0"` + Batcher string `yaml:"Batcher" default:"jaeger"` + // OtlpHeaders represents the headers for OTLP gRPC or HTTP transport. + // For example: + // uptrace-dsn: 'http://project2_secret_token@localhost:14317/2' + OtlpHeaders map[string]string `yaml:"OtlpHeaders"` + // OtlpHttpPath represents the path for OTLP HTTP transport. + // For example + // /v1/traces + OtlpHttpPath string `yaml:"OtlpHttpPath"` + // OtlpHttpSecure represents the scheme to use for OTLP HTTP transport. + OtlpHttpSecure bool `yaml:"OtlpHttpSecure"` + // Disabled indicates whether StartAgent starts the agent. + Disabled bool `yaml:"Disabled"` +} diff --git a/pkg/trace/message.go b/pkg/trace/message.go new file mode 100644 index 0000000..93c52c7 --- /dev/null +++ b/pkg/trace/message.go @@ -0,0 +1,38 @@ +package trace + +import ( + "context" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "google.golang.org/protobuf/proto" +) + +const messageEvent = "message" + +var ( + // MessageSent is the type of sent messages. + MessageSent = messageType(RPCMessageTypeSent) + // MessageReceived is the type of received messages. + MessageReceived = messageType(RPCMessageTypeReceived) +) + +type messageType attribute.KeyValue + +// Event adds an event of the messageType to the span associated with the +// passed context with id and size (if message is a proto message). +func (m messageType) Event(ctx context.Context, id int, message any) { + span := trace.SpanFromContext(ctx) + if p, ok := message.(proto.Message); ok { + span.AddEvent(messageEvent, trace.WithAttributes( + attribute.KeyValue(m), + RPCMessageIDKey.Int(id), + RPCMessageUncompressedSizeKey.Int(proto.Size(p)), + )) + } else { + span.AddEvent(messageEvent, trace.WithAttributes( + attribute.KeyValue(m), + RPCMessageIDKey.Int(id), + )) + } +} diff --git a/pkg/trace/message_test.go b/pkg/trace/message_test.go new file mode 100644 index 0000000..b1de239 --- /dev/null +++ b/pkg/trace/message_test.go @@ -0,0 +1,76 @@ +package trace + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/dynamicpb" +) + +func TestMessageType_Event(t *testing.T) { + ctx, s := otel.Tracer(TraceName).Start(context.Background(), "test") + span := mockSpan{Span: s} + ctx = trace.ContextWithSpan(ctx, &span) + MessageReceived.Event(ctx, 1, "foo") + assert.Equal(t, messageEvent, span.name) + assert.NotEmpty(t, span.options) +} + +func TestMessageType_EventProtoMessage(t *testing.T) { + var span mockSpan + var message mockMessage + ctx := trace.ContextWithSpan(context.Background(), &span) + MessageReceived.Event(ctx, 1, message) + assert.Equal(t, messageEvent, span.name) + assert.NotEmpty(t, span.options) +} + +type mockSpan struct { + trace.Span + name string + options []trace.EventOption +} + +func (m *mockSpan) End(_ ...trace.SpanEndOption) { +} + +func (m *mockSpan) AddEvent(name string, options ...trace.EventOption) { + m.name = name + m.options = options +} + +func (m *mockSpan) IsRecording() bool { + return false +} + +func (m *mockSpan) RecordError(_ error, _ ...trace.EventOption) { +} + +func (m *mockSpan) SpanContext() trace.SpanContext { + panic("implement me") +} + +func (m *mockSpan) SetStatus(_ codes.Code, _ string) { +} + +func (m *mockSpan) SetName(_ string) { +} + +func (m *mockSpan) SetAttributes(_ ...attribute.KeyValue) { +} + +func (m *mockSpan) TracerProvider() trace.TracerProvider { + return nil +} + +type mockMessage struct{} + +func (m mockMessage) ProtoReflect() protoreflect.Message { + return new(dynamicpb.Message) +} diff --git a/pkg/trace/propagation.go b/pkg/trace/propagation.go new file mode 100644 index 0000000..e092003 --- /dev/null +++ b/pkg/trace/propagation.go @@ -0,0 +1,11 @@ +package trace + +import ( + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" +) + +func init() { + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, propagation.Baggage{})) +} diff --git a/pkg/trace/resource.go b/pkg/trace/resource.go new file mode 100644 index 0000000..84d4d45 --- /dev/null +++ b/pkg/trace/resource.go @@ -0,0 +1,10 @@ +package trace + +import "go.opentelemetry.io/otel/attribute" + +var attrResources = make([]attribute.KeyValue, 0) + +// AddResources add more resources in addition to configured trace name. +func AddResources(attrs ...attribute.KeyValue) { + attrResources = append(attrResources, attrs...) +} diff --git a/pkg/trace/tracer.go b/pkg/trace/tracer.go new file mode 100644 index 0000000..1e3ce0f --- /dev/null +++ b/pkg/trace/tracer.go @@ -0,0 +1,56 @@ +package trace + +import ( + "context" + + "go.opentelemetry.io/otel/baggage" + "go.opentelemetry.io/otel/propagation" + sdktrace "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc/metadata" +) + +// assert that metadataSupplier implements the TextMapCarrier interface +var _ propagation.TextMapCarrier = (*metadataSupplier)(nil) + +type metadataSupplier struct { + metadata *metadata.MD +} + +func (s *metadataSupplier) Get(key string) string { + values := s.metadata.Get(key) + if len(values) == 0 { + return "" + } + + return values[0] +} + +func (s *metadataSupplier) Set(key, value string) { + s.metadata.Set(key, value) +} + +func (s *metadataSupplier) Keys() []string { + out := make([]string, 0, len(*s.metadata)) + for key := range *s.metadata { + out = append(out, key) + } + + return out +} + +// Inject injects cross-cutting concerns from the ctx into the metadata. +func Inject(ctx context.Context, p propagation.TextMapPropagator, metadata *metadata.MD) { + p.Inject(ctx, &metadataSupplier{ + metadata: metadata, + }) +} + +// Extract extracts the metadata from ctx. +func Extract(ctx context.Context, p propagation.TextMapPropagator, metadata *metadata.MD) ( + baggage.Baggage, sdktrace.SpanContext) { + ctx = p.Extract(ctx, &metadataSupplier{ + metadata: metadata, + }) + + return baggage.FromContext(ctx), sdktrace.SpanContextFromContext(ctx) +} diff --git a/pkg/trace/tracer_test.go b/pkg/trace/tracer_test.go new file mode 100644 index 0000000..bb77d38 --- /dev/null +++ b/pkg/trace/tracer_test.go @@ -0,0 +1,356 @@ +package trace + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc/metadata" +) + +const ( + traceIDStr = "4bf92f3577b34da6a3ce929d0e0e4736" + spanIDStr = "00f067aa0ba902b7" +) + +var ( + traceID = mustTraceIDFromHex(traceIDStr) + spanID = mustSpanIDFromHex(spanIDStr) +) + +func mustTraceIDFromHex(s string) (t trace.TraceID) { + var err error + t, err = trace.TraceIDFromHex(s) + if err != nil { + panic(err) + } + return +} + +func mustSpanIDFromHex(s string) (t trace.SpanID) { + var err error + t, err = trace.SpanIDFromHex(s) + if err != nil { + panic(err) + } + return +} + +func TestExtractValidTraceContext(t *testing.T) { + stateStr := "key1=value1,key2=value2" + state, err := trace.ParseTraceState(stateStr) + require.NoError(t, err) + + tests := []struct { + name string + traceparent string + tracestate string + sc trace.SpanContext + }{ + { + name: "not sampled", + traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + Remote: true, + }), + }, + { + name: "sampled", + traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + Remote: true, + }), + }, + { + name: "valid tracestate", + traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", + tracestate: stateStr, + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceState: state, + Remote: true, + }), + }, + { + name: "invalid tracestate perserves traceparent", + traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", + tracestate: "invalid$@#=invalid", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + Remote: true, + }), + }, + { + name: "future version not sampled", + traceparent: "02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + Remote: true, + }), + }, + { + name: "future version sampled", + traceparent: "02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + Remote: true, + }), + }, + { + name: "future version sample bit set", + traceparent: "02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-09", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + Remote: true, + }), + }, + { + name: "future version sample bit not set", + traceparent: "02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-08", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + Remote: true, + }), + }, + { + name: "future version additional data", + traceparent: "02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00-XYZxsf09", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + Remote: true, + }), + }, + { + name: "B3 format ending in dash", + traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00-", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + Remote: true, + }), + }, + { + name: "future version B3 format ending in dash", + traceparent: "03-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00-", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + Remote: true, + }), + }, + } + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, propagation.Baggage{})) + propagator := otel.GetTextMapPropagator() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + md := metadata.MD{} + md.Set("traceparent", tt.traceparent) + md.Set("tracestate", tt.tracestate) + _, spanCtx := Extract(ctx, propagator, &md) + assert.Equal(t, tt.sc, spanCtx) + }) + } +} + +func TestExtractInvalidTraceContext(t *testing.T) { + tests := []struct { + name string + header string + }{ + { + name: "wrong version length", + header: "0000-00000000000000000000000000000000-0000000000000000-01", + }, + { + name: "wrong trace ID length", + header: "00-ab00000000000000000000000000000000-cd00000000000000-01", + }, + { + name: "wrong span ID length", + header: "00-ab000000000000000000000000000000-cd0000000000000000-01", + }, + { + name: "wrong trace flag length", + header: "00-ab000000000000000000000000000000-cd00000000000000-0100", + }, + { + name: "bogus version", + header: "qw-00000000000000000000000000000000-0000000000000000-01", + }, + { + name: "bogus trace ID", + header: "00-qw000000000000000000000000000000-cd00000000000000-01", + }, + { + name: "bogus span ID", + header: "00-ab000000000000000000000000000000-qw00000000000000-01", + }, + { + name: "bogus trace flag", + header: "00-ab000000000000000000000000000000-cd00000000000000-qw", + }, + { + name: "upper case version", + header: "A0-00000000000000000000000000000000-0000000000000000-01", + }, + { + name: "upper case trace ID", + header: "00-AB000000000000000000000000000000-cd00000000000000-01", + }, + { + name: "upper case span ID", + header: "00-ab000000000000000000000000000000-CD00000000000000-01", + }, + { + name: "upper case trace flag", + header: "00-ab000000000000000000000000000000-cd00000000000000-A1", + }, + { + name: "zero trace ID and span ID", + header: "00-00000000000000000000000000000000-0000000000000000-01", + }, + { + name: "trace-flag unused bits set", + header: "00-ab000000000000000000000000000000-cd00000000000000-09", + }, + { + name: "missing options", + header: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7", + }, + { + name: "empty options", + header: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-", + }, + } + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, propagation.Baggage{})) + propagator := otel.GetTextMapPropagator() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + md := metadata.MD{} + md.Set("traceparent", tt.header) + _, spanCtx := Extract(ctx, propagator, &md) + assert.Equal(t, trace.SpanContext{}, spanCtx) + }) + } +} + +func TestInjectValidTraceContext(t *testing.T) { + stateStr := "key1=value1,key2=value2" + state, err := trace.ParseTraceState(stateStr) + require.NoError(t, err) + + tests := []struct { + name string + traceparent string + tracestate string + sc trace.SpanContext + }{ + { + name: "not sampled", + traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + Remote: true, + }), + }, + { + name: "sampled", + traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + Remote: true, + }), + }, + { + name: "unsupported trace flag bits dropped", + traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: 0xff, + Remote: true, + }), + }, + { + name: "with tracestate", + traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", + tracestate: stateStr, + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceState: state, + Remote: true, + }), + }, + } + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, propagation.Baggage{})) + propagator := otel.GetTextMapPropagator() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + ctx = trace.ContextWithRemoteSpanContext(ctx, tt.sc) + + want := metadata.MD{} + want.Set("traceparent", tt.traceparent) + if len(tt.tracestate) > 0 { + want.Set("tracestate", tt.tracestate) + } + + md := metadata.MD{} + Inject(ctx, propagator, &md) + assert.Equal(t, want, md) + + mm := &metadataSupplier{ + metadata: &md, + } + assert.NotEmpty(t, mm.Keys()) + }) + } +} + +func TestInvalidSpanContextDropped(t *testing.T) { + invalidSC := trace.SpanContext{} + require.False(t, invalidSC.IsValid()) + ctx := trace.ContextWithRemoteSpanContext(context.Background(), invalidSC) + + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, propagation.Baggage{})) + propagator := otel.GetTextMapPropagator() + + md := metadata.MD{} + Inject(ctx, propagator, &md) + mm := &metadataSupplier{ + metadata: &md, + } + assert.Empty(t, mm.Keys()) + assert.Equal(t, "", mm.Get("traceparent"), "injected invalid SpanContext") +} diff --git a/pkg/trace/tracetest/tracetest.go b/pkg/trace/tracetest/tracetest.go new file mode 100644 index 0000000..fb8b17d --- /dev/null +++ b/pkg/trace/tracetest/tracetest.go @@ -0,0 +1,21 @@ +package tracetest + +import ( + "testing" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" +) + +// NewInMemoryExporter returns a new InMemoryExporter +// and sets it as the global for tests. +func NewInMemoryExporter(t *testing.T) *tracetest.InMemoryExporter { + me := tracetest.NewInMemoryExporter() + t.Cleanup(func() { + me.Reset() + }) + otel.SetTracerProvider(trace.NewTracerProvider(trace.WithSyncer(me))) + + return me +} diff --git a/pkg/trace/utils.go b/pkg/trace/utils.go new file mode 100644 index 0000000..2756284 --- /dev/null +++ b/pkg/trace/utils.go @@ -0,0 +1,89 @@ +package trace + +import ( + "context" + "net" + "strings" + + ptrace "github.com/perfect-panel/ppanel-server/internal/trace" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" + "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc/peer" +) + +const localhost = "127.0.0.1" + +var ( + // SpanIDFromContext returns the span id from ctx. + SpanIDFromContext = ptrace.SpanIDFromContext + // TraceIDFromContext returns the trace id from ctx. + TraceIDFromContext = ptrace.TraceIDFromContext +) + +// ParseFullMethod returns the method name and attributes. +func ParseFullMethod(fullMethod string) (string, []attribute.KeyValue) { + name := strings.TrimLeft(fullMethod, "/") + parts := strings.SplitN(name, "/", 2) + if len(parts) != 2 { + // Invalid format, does not follow `/package.service/method`. + return name, []attribute.KeyValue(nil) + } + + var attrs []attribute.KeyValue + if service := parts[0]; service != "" { + attrs = append(attrs, semconv.RPCServiceKey.String(service)) + } + if method := parts[1]; method != "" { + attrs = append(attrs, semconv.RPCMethodKey.String(method)) + } + + return name, attrs +} + +// PeerAttr returns the peer attributes. +func PeerAttr(addr string) []attribute.KeyValue { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return nil + } + + if len(host) == 0 { + host = localhost + } + + return []attribute.KeyValue{ + semconv.NetPeerIPKey.String(host), + semconv.NetPeerPortKey.String(port), + } +} + +// PeerFromCtx returns the peer from ctx. +func PeerFromCtx(ctx context.Context) string { + p, ok := peer.FromContext(ctx) + if !ok || p == nil { + return "" + } + + return p.Addr.String() +} + +// SpanInfo returns the span info. +func SpanInfo(fullMethod, peerAddress string) (string, []attribute.KeyValue) { + attrs := []attribute.KeyValue{RPCSystemGRPC} + name, mAttrs := ParseFullMethod(fullMethod) + attrs = append(attrs, mAttrs...) + attrs = append(attrs, PeerAttr(peerAddress)...) + return name, attrs +} + +// TracerFromContext returns a tracer in ctx, otherwise returns a global tracer. +func TracerFromContext(ctx context.Context) (tracer trace.Tracer) { + if span := trace.SpanFromContext(ctx); span.SpanContext().IsValid() { + tracer = span.TracerProvider().Tracer(TraceName) + } else { + tracer = otel.Tracer(TraceName) + } + return +} diff --git a/pkg/trace/utils_test.go b/pkg/trace/utils_test.go new file mode 100644 index 0000000..089479e --- /dev/null +++ b/pkg/trace/utils_test.go @@ -0,0 +1,204 @@ +package trace + +import ( + "context" + "net" + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" + "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc/peer" +) + +func TestPeerFromContext(t *testing.T) { + addrs, err := net.InterfaceAddrs() + assert.Nil(t, err) + assert.NotEmpty(t, addrs) + tests := []struct { + name string + ctx context.Context + empty bool + }{ + { + name: "empty", + ctx: context.Background(), + empty: true, + }, + { + name: "nil", + ctx: peer.NewContext(context.Background(), nil), + empty: true, + }, + { + name: "with value", + ctx: peer.NewContext(context.Background(), &peer.Peer{ + Addr: addrs[0], + }), + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + addr := PeerFromCtx(test.ctx) + assert.Equal(t, test.empty, len(addr) == 0) + }) + } +} + +func TestParseFullMethod(t *testing.T) { + tests := []struct { + fullMethod string + name string + attr []attribute.KeyValue + }{ + { + fullMethod: "/grpc.test.EchoService/Echo", + name: "grpc.test.EchoService/Echo", + attr: []attribute.KeyValue{ + semconv.RPCServiceKey.String("grpc.test.EchoService"), + semconv.RPCMethodKey.String("Echo"), + }, + }, { + fullMethod: "/com.example.ExampleRmiService/exampleMethod", + name: "com.example.ExampleRmiService/exampleMethod", + attr: []attribute.KeyValue{ + semconv.RPCServiceKey.String("com.example.ExampleRmiService"), + semconv.RPCMethodKey.String("exampleMethod"), + }, + }, { + fullMethod: "/MyCalcService.Calculator/Add", + name: "MyCalcService.Calculator/Add", + attr: []attribute.KeyValue{ + semconv.RPCServiceKey.String("MyCalcService.Calculator"), + semconv.RPCMethodKey.String("Add"), + }, + }, { + fullMethod: "/MyServiceReference.ICalculator/Add", + name: "MyServiceReference.ICalculator/Add", + attr: []attribute.KeyValue{ + semconv.RPCServiceKey.String("MyServiceReference.ICalculator"), + semconv.RPCMethodKey.String("Add"), + }, + }, { + fullMethod: "/MyServiceWithNoPackage/theMethod", + name: "MyServiceWithNoPackage/theMethod", + attr: []attribute.KeyValue{ + semconv.RPCServiceKey.String("MyServiceWithNoPackage"), + semconv.RPCMethodKey.String("theMethod"), + }, + }, { + fullMethod: "/pkg.svr", + name: "pkg.svr", + attr: []attribute.KeyValue(nil), + }, { + fullMethod: "/pkg.svr/", + name: "pkg.svr/", + attr: []attribute.KeyValue{ + semconv.RPCServiceKey.String("pkg.svr"), + }, + }, + } + + for _, test := range tests { + n, a := ParseFullMethod(test.fullMethod) + assert.Equal(t, test.name, n) + assert.Equal(t, test.attr, a) + } +} + +func TestSpanInfo(t *testing.T) { + val, kvs := SpanInfo("/fullMethod", "remote") + assert.Equal(t, "fullMethod", val) + assert.NotEmpty(t, kvs) +} + +func TestPeerAttr(t *testing.T) { + tests := []struct { + name string + addr string + expect []attribute.KeyValue + }{ + { + name: "empty", + }, + { + name: "port only", + addr: ":8080", + expect: []attribute.KeyValue{ + semconv.NetPeerIPKey.String(localhost), + semconv.NetPeerPortKey.String("8080"), + }, + }, + { + name: "port only", + addr: "192.168.0.2:8080", + expect: []attribute.KeyValue{ + semconv.NetPeerIPKey.String("192.168.0.2"), + semconv.NetPeerPortKey.String("8080"), + }, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + kvs := PeerAttr(test.addr) + assert.EqualValues(t, test.expect, kvs) + }) + } +} + +func TestTracerFromContext(t *testing.T) { + traceFn := func(ctx context.Context, hasTraceId bool) { + spanContext := trace.SpanContextFromContext(ctx) + assert.Equal(t, spanContext.IsValid(), hasTraceId) + parentTraceId := spanContext.TraceID().String() + + tracer := TracerFromContext(ctx) + _, span := tracer.Start(ctx, "b") + defer span.End() + + spanContext = span.SpanContext() + assert.True(t, spanContext.IsValid()) + if hasTraceId { + assert.Equal(t, parentTraceId, spanContext.TraceID().String()) + } + + } + + t.Run("context", func(t *testing.T) { + opts := []sdktrace.TracerProviderOption{ + // Set the sampling rate based on the parent span to 100% + sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(1))), + // Record information about this application in a Resource. + sdktrace.WithResource(resource.NewSchemaless(semconv.ServiceNameKey.String("test"))), + } + tp = sdktrace.NewTracerProvider(opts...) + otel.SetTracerProvider(tp) + ctx, span := tp.Tracer(TraceName).Start(context.Background(), "a") + + defer span.End() + traceFn(ctx, true) + }) + + t.Run("global", func(t *testing.T) { + opts := []sdktrace.TracerProviderOption{ + // Set the sampling rate based on the parent span to 100% + sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(1))), + // Record information about this application in a Resource. + sdktrace.WithResource(resource.NewSchemaless(semconv.ServiceNameKey.String("test"))), + } + tp = sdktrace.NewTracerProvider(opts...) + otel.SetTracerProvider(tp) + + traceFn(context.Background(), false) + }) +} diff --git a/pkg/trace/vars.go b/pkg/trace/vars.go new file mode 100644 index 0000000..4312436 --- /dev/null +++ b/pkg/trace/vars.go @@ -0,0 +1,9 @@ +package trace + +import "net/http" + +// RequestIdKey is the request id header, has a unique association to a trace id. +// The Request ID uses the UUID format because, first, it is more in line with industry standards, +// second, it guarantees the privacy and security of the Trace ID, +// and third, it provides standardization capabilities for subsequent distribution. +var RequestIdKey = http.CanonicalHeaderKey("x-request-id") diff --git a/pkg/traffic/convert.go b/pkg/traffic/convert.go new file mode 100644 index 0000000..8c3b10c --- /dev/null +++ b/pkg/traffic/convert.go @@ -0,0 +1,66 @@ +package traffic + +import "fmt" + +const ( + bitsInTiB = 1024.0 * 1024.0 * 1024.0 * 1024.0 + bitsInTb = 1000.0 * 1000.0 * 1000.0 * 1000.0 + bitsInGiB = 1024.0 * 1024.0 * 1024.0 + bitsInGb = 1000.0 * 1000.0 * 1000.0 + bitsInMiB = 1024.0 * 1024.0 + bitsInMb = 1000.0 * 1000.0 + + Mb = "Mb" + MiB = "MiB" + Gb = "Gb" + GiB = "GiB" + Tb = "Tb" + TiB = "TiB" +) + +// Convert converts the given traffic in bits to the specified unit ("MiB" or "GiB") +func Convert(bits int64, unit string) float64 { + switch unit { + case Mb: + return float64(bits) / bitsInMb + case MiB: + return float64(bits) / bitsInMiB + case GiB: + return float64(bits) / bitsInGiB + case Gb: + return float64(bits) / bitsInGb + case TiB: + return float64(bits) / bitsInTiB + case Tb: + return float64(bits) / bitsInTb + default: + return 0 // Return 0 for unsupported units + } +} + +// AutoConvert converts the given traffic in bits to the largest possible unit and returns a formatted string +func AutoConvert(bits int64, useBinary bool) string { + if useBinary { + switch { + case float64(bits) >= bitsInTiB: + return fmt.Sprintf("%.2f TiB", float64(bits)/bitsInTiB) + case float64(bits) >= bitsInGiB: + return fmt.Sprintf("%.2f GiB", float64(bits)/bitsInGiB) + case float64(bits) >= bitsInMiB: + return fmt.Sprintf("%.2f MiB", float64(bits)/bitsInMiB) + default: + return fmt.Sprintf("%d bits", bits) + } + } else { + switch { + case float64(bits) >= bitsInTb: + return fmt.Sprintf("%.2f Tb", float64(bits)/bitsInTb) + case float64(bits) >= bitsInGb: + return fmt.Sprintf("%.2f Gb", float64(bits)/bitsInGb) + case float64(bits) >= bitsInMb: + return fmt.Sprintf("%.2f Mb", float64(bits)/bitsInMb) + default: + return fmt.Sprintf("%d bits", bits) + } + } +} diff --git a/pkg/turnstile/client.go b/pkg/turnstile/client.go new file mode 100644 index 0000000..a40db30 --- /dev/null +++ b/pkg/turnstile/client.go @@ -0,0 +1,47 @@ +package turnstile + +import ( + "context" + "time" +) + +// Config is the configuration for the service. +type Config struct { + // Secret is the secret key used to verify the token. + // This is required. + Secret string + + // Timeout is the timeout for the service. + // This is optional. + // Default: 10 seconds + Timeout time.Duration +} + +// Service is the interface for the service. +// It is used to verify the token. +// It is also used to generate a random UUID. +type Service interface { + // Verify is used to verify the token. + // It returns true if the token is valid. + // It returns false if the token is invalid. + // It returns an error if there was an error verifying the token. + Verify(ctx context.Context, token string, ip string) (bool, error) + + // VerifyIdempotent is used to verify the token. + // The key parameter is used to ensure idempotency. + // You may use the RandomUUID method to generate a random UUID. + // It returns true if the token is valid. + // It returns false if the token is invalid. + // It returns an error if there was an error verifying the token. + VerifyIdempotent(ctx context.Context, token string, ip string, key string) (bool, error) + + // RandomUUID is used to generate a random UUID. + // It returns a random UUID. + RandomUUID() string +} + +// New is used to create a new service. +// It returns a new service. +func New(config Config) Service { + return newService(config) +} diff --git a/pkg/turnstile/service.go b/pkg/turnstile/service.go new file mode 100644 index 0000000..e3be9a7 --- /dev/null +++ b/pkg/turnstile/service.go @@ -0,0 +1,76 @@ +package turnstile + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/json" + "fmt" + "mime/multipart" + "net/http" + "time" +) + +type service struct { + timeout time.Duration + secret string + url string +} + +func newService(config Config) Service { + if config.Timeout == 0 { + config.Timeout = 10 * time.Second + } + return &service{ + secret: config.Secret, + timeout: config.Timeout, + url: "https://challenges.cloudflare.com/turnstile/v0/siteverify", + } +} + +func (s *service) Verify(ctx context.Context, token string, ip string) (bool, error) { + return s.verify(ctx, s.secret, token, ip, "") +} + +func (s *service) VerifyIdempotent(ctx context.Context, token string, ip string, key string) (bool, error) { + return s.verify(ctx, s.secret, token, ip, key) +} + +func (s *service) RandomUUID() string { + uuid := make([]byte, 16) + _, _ = rand.Read(uuid) + uuid[6] = (uuid[6] & 0x0f) | 0x40 + uuid[8] = (uuid[8] & 0x3f) | 0x80 + return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]) +} + +func (s *service) verify(ctx context.Context, secret string, token string, ip string, key string) (bool, error) { + _, cancel := context.WithTimeout(ctx, s.timeout) + defer cancel() + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + _ = writer.WriteField("secret", secret) + _ = writer.WriteField("response", token) + _ = writer.WriteField("remoteip", ip) + if key != "" { + _ = writer.WriteField("idempotency_key", key) + } + _ = writer.Close() + client := &http.Client{} + req, _ := http.NewRequest("POST", s.url, body) + req.Header.Set("Content-Type", writer.FormDataContentType()) + firstResult, err := client.Do(req) + if err != nil { + return false, err + } + defer firstResult.Body.Close() + firstOutcome := make(map[string]interface{}) + err = json.NewDecoder(firstResult.Body).Decode(&firstOutcome) + if err != nil { + return false, err + } + if success, ok := firstOutcome["success"].(bool); ok && success { + return true, nil + } + return false, nil +} diff --git a/pkg/uuidx/uuid.go b/pkg/uuidx/uuid.go new file mode 100644 index 0000000..baaaf75 --- /dev/null +++ b/pkg/uuidx/uuid.go @@ -0,0 +1,96 @@ +package uuidx + +import ( + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" + "time" + + "github.com/gofrs/uuid/v5" + "github.com/perfect-panel/ppanel-server/pkg/random" +) + +// NewUUID returns a new UUID. +func NewUUID() uuid.UUID { + id, err := uuid.NewV7() + if err != nil { + fmt.Println("fail to generate UUID", err.Error()) + return uuid.UUID{} + } + return id +} + +// ParseUUIDSlice parses the UUID string slice to UUID slice. +func ParseUUIDSlice(ids []string) []uuid.UUID { + var result []uuid.UUID + for _, v := range ids { + p, err := uuid.FromString(v) + if err != nil { + return nil + } + result = append(result, p) + } + return result +} + +// ParseUUIDString parses UUID string to UUID type. +func ParseUUIDString(id string) uuid.UUID { + result, err := uuid.FromString(id) + if err != nil { + return uuid.UUID{} + } + return result +} + +// ParseUUIDSliceToPointer parses the UUID string slice to UUID pointer slice. +func ParseUUIDSliceToPointer(ids []string) []*uuid.UUID { + var result []*uuid.UUID + for _, v := range ids { + p, err := uuid.FromString(v) + if err != nil { + return nil + } + result = append(result, &p) + } + return result +} + +// ParseUUIDStringToPointer parses UUID string to UUID pointer. +func ParseUUIDStringToPointer(id *string) *uuid.UUID { + if id == nil { + return nil + } + + result, err := uuid.FromString(*id) + if err != nil { + return nil + } + return &result +} + +func UserInviteCode(id int64) string { + return "u" + random.EncodeBase62(id+time.Now().UnixMilli()) +} + +func AffiliateInviteCode(id int64) string { + return "A" + random.EncodeBase62(id) +} + +func SubscribeToken(orderNo string) string { + hash := sha256.Sum256([]byte(orderNo)) + return hex.EncodeToString(hash[:16]) +} + +func UUIDToBase64(uuid string, length int) string { + // 截取 uuid 的前 length 个字符 + if length > len(uuid) { + length = len(uuid) + } + shortUUID := uuid[:length] + + // 对截取的字符串进行 Base64 编码 + encoded := base64.StdEncoding.EncodeToString([]byte(shortUUID)) + + return encoded +} diff --git a/pkg/uuidx/uuid_test.go b/pkg/uuidx/uuid_test.go new file mode 100644 index 0000000..ca83b9a --- /dev/null +++ b/pkg/uuidx/uuid_test.go @@ -0,0 +1,98 @@ +// Copyright 2023 The Ryan SU Authors (https://github.com/suyuan32). All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package uuidx + +import ( + "fmt" + "reflect" + "testing" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/random" + "github.com/perfect-panel/ppanel-server/pkg/snowflake" + + "github.com/gofrs/uuid/v5" +) + +func TestParseUUIDSlice(t *testing.T) { + type args struct { + ids []string + } + tests := []struct { + name string + args args + want []uuid.UUID + }{ + { + name: "test1", + args: args{ids: []string{"123"}}, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ParseUUIDSlice(tt.args.ids); !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseUUIDSlice() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestParseUUIDString(t *testing.T) { + type args struct { + id string + } + tests := []struct { + name string + args args + want uuid.UUID + }{ + { + name: "test1", + args: args{id: "123456"}, + want: uuid.UUID{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ParseUUIDString(tt.args.id); !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseUUIDString() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestT1(t *testing.T) { + valMap := make(map[string]struct{}) + num := 100 + for i := 0; i < num; i++ { + exCode := random.StrToDashedString(random.EncodeBase62(snowflake.GetID())) + valMap[exCode] = struct{}{} + } + t.Log(len(valMap)) +} + +func TestAffCode(t *testing.T) { + + code := AffiliateInviteCode(time.Now().UnixMilli()) + + fmt.Println(code) +} + +func TestSubscribeMarkCode(t *testing.T) { + orderNo := "20241213222445955" + code := SubscribeToken(orderNo) + fmt.Println(code) +} diff --git a/pkg/xerr/errCode.go b/pkg/xerr/errCode.go new file mode 100644 index 0000000..8b238a1 --- /dev/null +++ b/pkg/xerr/errCode.go @@ -0,0 +1,122 @@ +package xerr + +/** (The first 3 digits represent the business, and the last three digits represent the specific function) **/ + +// General error code +const ( + SUCCESS uint32 = 200 + ERROR uint32 = 500 +) + +// Database error +const ( + DatabaseQueryError uint32 = 10001 + DatabaseUpdateError uint32 = 10002 + DatabaseInsertError uint32 = 10003 + DatabaseDeletedError uint32 = 10004 +) + +// User error +const ( + UserExist uint32 = 20001 + UserNotExist uint32 = 20002 + UserPasswordError uint32 = 20003 + UserDisabled uint32 = 20004 + InsufficientBalance uint32 = 20005 + StopRegister uint32 = 20006 + TelegramNotBound uint32 = 20007 + UserNotBindOauth uint32 = 20008 + InviteCodeError uint32 = 20009 +) + +// Node error +const ( + NodeExist uint32 = 30001 + NodeNotExist uint32 = 30002 + NodeGroupExist uint32 = 30003 + NodeGroupNotExist uint32 = 30004 + NodeGroupNotEmpty uint32 = 30005 +) + +// Request error +const ( + InvalidParams uint32 = 400 + TooManyRequests uint32 = 401 + ErrorTokenEmpty uint32 = 40002 + ErrorTokenInvalid uint32 = 40003 + ErrorTokenExpire uint32 = 40004 + InvalidAccess uint32 = 40005 + InvalidCiphertext uint32 = 40006 +) + +//coupon error + +const ( + CouponNotExist uint32 = 50001 + CouponUsed uint32 = 50002 + CouponNotMatch uint32 = 50003 +) + +// Subscribe + +const ( + SubscribeExpired uint32 = 60001 + SubscribeNotAvailable uint32 = 60002 + UserSubscribeExist uint32 = 60003 + SubscribeIsUsedError uint32 = 60004 + SingleSubscribeModeExceedsLimit uint32 = 60005 + SubscribeQuotaLimit uint32 = 60006 +) + +// Auth error + +const ( + VerifyCodeError uint32 = 70001 +) + +// equipment error + +const ( + QueueEnqueueError uint32 = 80001 +) + +// System error + +const ( + DebugModeError uint32 = 90001 +) + +const ( + SendSmsError uint32 = 90002 + SmsNotEnabled uint32 = 90003 + EmailNotEnabled uint32 = 90004 +) + +const ( + GetAuthenticatorError uint32 = 90005 + AuthenticatorNotSupportedError uint32 = 90006 + TelephoneAreaCodeIsEmpty uint32 = 90007 + TodaySendCountExceedsLimit uint32 = 90015 +) + +const ( + PasswordIsEmpty uint32 = 90008 + AreaCodeIsEmpty uint32 = 90009 + PasswordOrVerificationCodeRequired uint32 = 90010 + EmailExist uint32 = 90011 + TelephoneExist uint32 = 90012 + DeviceExist uint32 = 90013 + TelephoneError uint32 = 90014 +) +const ( + DeviceNotExist uint32 = 90017 + UseridNotMatch uint32 = 90018 +) + +const ( + OrderNotExist uint32 = 61001 + PaymentMethodNotFound uint32 = 61002 + OrderStatusError uint32 = 61003 + InsufficientOfPeriod uint32 = 61004 + ExistAvailableTraffic uint32 = 61005 +) diff --git a/pkg/xerr/errMsg.go b/pkg/xerr/errMsg.go new file mode 100644 index 0000000..b0fa9a8 --- /dev/null +++ b/pkg/xerr/errMsg.go @@ -0,0 +1,104 @@ +package xerr + +var message map[uint32]string + +func init() { + message = make(map[uint32]string) + message = map[uint32]string{ + // General error + SUCCESS: "Success", + ERROR: "Internal Server Error", + // parameter error + TooManyRequests: "Too Many Requests", + InvalidParams: "Param Error", + ErrorTokenEmpty: "User token is empty", + ErrorTokenInvalid: "User token is invalid", + ErrorTokenExpire: "User token is expired", + InvalidAccess: "Invalid access", + InvalidCiphertext: "Invalid ciphertext", + // Database error + DatabaseQueryError: "Database query error", + DatabaseUpdateError: "Database update error", + DatabaseInsertError: "Database insert error", + DatabaseDeletedError: "Database deleted error", + + // User error + UserExist: "User already exists", + UserNotExist: "User does not exist", + UserPasswordError: "User password error", + UserDisabled: "User disabled", + InsufficientBalance: "Insufficient balance", + StopRegister: "Stop register", + TelegramNotBound: "Telegram not bound ", + UserNotBindOauth: "User not bind oauth method", + InviteCodeError: "Invite code error", + + // Node error + NodeExist: "Node already exists", + NodeNotExist: "Node does not exist", + NodeGroupExist: "Node group already exists", + NodeGroupNotExist: "Node group does not exist", + NodeGroupNotEmpty: "Node group is not empty", + + //coupon error + CouponNotExist: "Coupon does not exist", + CouponUsed: "Coupon has been used", + CouponNotMatch: "Coupon does not match", + + // Subscribe + SubscribeExpired: "Subscribe is expired", + SubscribeNotAvailable: "Subscribe is not available", + UserSubscribeExist: "User has subscription", + SubscribeIsUsedError: "Subscribe is used", + SingleSubscribeModeExceedsLimit: "Single subscribe mode exceeds limit", + SubscribeQuotaLimit: "Subscribe quota limit", + + // auth error + VerifyCodeError: "Verify code error", + + // EnqueueError + QueueEnqueueError: " Queue enqueue error", + + // System error + DebugModeError: "Debug mode is enabled", + + GetAuthenticatorError: "Unsupported login method", + AuthenticatorNotSupportedError: "The authenticator does not support this method", + + TelephoneAreaCodeIsEmpty: "Telephone area code is empty", + TodaySendCountExceedsLimit: "This account has reached the limit of sending times today", + SmsNotEnabled: "Telephone login is not enabled", + EmailNotEnabled: "Email function is not enabled yet", + PasswordOrVerificationCodeRequired: "Password or verification code required", + EmailExist: "Email already exists", + TelephoneExist: "Telephone already exists", + DeviceExist: "device exists", + PasswordIsEmpty: "password is empty", + TelephoneError: "telephone number error", + DeviceNotExist: "Device does not exist", + UseridNotMatch: "Userid not match", + + // Order error + OrderNotExist: "Order does not exist", + PaymentMethodNotFound: "Payment method not found", + OrderStatusError: "Order status error", + InsufficientOfPeriod: "Insufficient number of period", + } + +} + +func MapErrMsg(errCode uint32) string { + if msg, ok := message[errCode]; ok { + return msg + } else { + return "Internal Server Error" + } +} + +func IsCodeErr(errCode uint32) bool { + if _, ok := message[errCode]; ok { + return true + } else { + return false + } +} diff --git a/pkg/xerr/errors.go b/pkg/xerr/errors.go new file mode 100644 index 0000000..f9930d7 --- /dev/null +++ b/pkg/xerr/errors.go @@ -0,0 +1,42 @@ +package xerr + +import ( + "errors" + "fmt" +) + +/** +General common fixed error +*/ + +type CodeError struct { + errCode uint32 + errMsg string +} + +var StatusNotModified = errors.New("304 Not Modified") + +// GetErrCode returns the error code displayed to the front end +func (e *CodeError) GetErrCode() uint32 { + return e.errCode +} + +// GetErrMsg returns the error message displayed to the front end +func (e *CodeError) GetErrMsg() string { + return e.errMsg +} + +func (e *CodeError) Error() string { + return fmt.Sprintf("ErrCode:%d,ErrMsg:%s", e.errCode, e.errMsg) +} + +func NewErrCodeMsg(errCode uint32, errMsg string) *CodeError { + return &CodeError{errCode: errCode, errMsg: errMsg} +} +func NewErrCode(errCode uint32) *CodeError { + return &CodeError{errCode: errCode, errMsg: MapErrMsg(errCode)} +} + +func NewErrMsg(errMsg string) *CodeError { + return &CodeError{errCode: ERROR, errMsg: errMsg} +} diff --git a/ppanel.api b/ppanel.api new file mode 100644 index 0000000..ca98a2f --- /dev/null +++ b/ppanel.api @@ -0,0 +1,48 @@ +syntax = "v1" + +info ( + title: "ppanel API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import ( + "apis/common.api" + "apis/node/node.api" + "apis/auth/auth.api" + "apis/admin/system.api" + "apis/admin/user.api" + "apis/admin/auth.api" + "apis/admin/server.api" + "apis/admin/subscribe.api" + "apis/admin/payment.api" + "apis/admin/coupon.api" + "apis/admin/order.api" + "apis/admin/ticket.api" + "apis/admin/announcement.api" + "apis/admin/document.api" + "apis/admin/tool.api" + "apis/admin/console.api" + "apis/admin/log.api" + "apis/admin/ads.api" + "apis/public/user.api" + "apis/public/subscribe.api" + "apis/public/order.api" + "apis/public/announcement.api" + "apis/public/ticket.api" + "apis/public/payment.api" + "apis/public/document.api" + "apis/public/portal.api" + "apis/app/auth.api" + "apis/app/user.api" + "apis/app/node.api" + "apis/app/ws.api" + "apis/app/order.api" + "apis/app/announcement.api" + "apis/app/payment.api" + "apis/app/document.api" + "apis/app/subscribe.api" +) + diff --git a/ppanel.go b/ppanel.go new file mode 100644 index 0000000..7c8624c --- /dev/null +++ b/ppanel.go @@ -0,0 +1,7 @@ +package main + +import "github.com/perfect-panel/ppanel-server/cmd" + +func main() { + cmd.Execute() +} diff --git a/ppanel.json b/ppanel.json new file mode 100644 index 0000000..2b9464f --- /dev/null +++ b/ppanel.json @@ -0,0 +1,7111 @@ +{ + "swagger": "2.0", + "info": { + "title": "", + "version": "" + }, + "schemes": [ + "http", + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/v1/admin/announcement/": { + "delete": { + "summary": "Delete announcement", + "operationId": "DeleteAnnouncement", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DeleteAnnouncementRequest" + } + } + ], + "tags": [ + "ppanel/admin/announcement" + ] + }, + "post": { + "summary": "Create announcement", + "operationId": "CreateAnnouncement", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateAnnouncementRequest" + } + } + ], + "tags": [ + "ppanel/admin/announcement" + ] + }, + "put": { + "summary": "Update announcement", + "operationId": "UpdateAnnouncement", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateAnnouncementRequest" + } + } + ], + "tags": [ + "ppanel/admin/announcement" + ] + } + }, + "/v1/admin/announcement/detail": { + "get": { + "summary": "Get announcement", + "operationId": "GetAnnouncement", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/Announcement" + } + } + } + ] + } + } + }, + "parameters": [ + { + "name": "id", + "in": "query", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "tags": [ + "ppanel/admin/announcement" + ] + } + }, + "/v1/admin/announcement/enable": { + "put": { + "summary": "Update announcement enable", + "operationId": "UpdateAnnouncementEnable", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateAnnouncementEnableRequest" + } + } + ], + "tags": [ + "ppanel/admin/announcement" + ] + } + }, + "/v1/admin/announcement/list": { + "get": { + "summary": "Get announcement list", + "operationId": "GetAnnouncementList", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetAnnouncementListResponse" + } + } + } + ] + } + } + }, + "parameters": [ + { + "name": "page", + "in": "query", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "size", + "in": "query", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "enable", + "in": "query", + "required": false, + "type": "boolean", + "format": "boolean" + }, + { + "name": "search", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "ppanel/admin/announcement" + ] + } + }, + "/v1/admin/coupon/": { + "delete": { + "summary": "Delete coupon", + "operationId": "DeleteCoupon", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DeleteCouponRequest" + } + } + ], + "tags": [ + "ppanel/admin/coupon" + ] + }, + "post": { + "summary": "Create coupon", + "operationId": "CreateCoupon", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateCouponRequest" + } + } + ], + "tags": [ + "ppanel/admin/coupon" + ] + }, + "put": { + "summary": "Update coupon", + "operationId": "UpdateCoupon", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateCouponRequest" + } + } + ], + "tags": [ + "ppanel/admin/coupon" + ] + } + }, + "/v1/admin/coupon/batch": { + "delete": { + "summary": "Batch delete coupon", + "operationId": "BatchDeleteCoupon", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/BatchDeleteCouponRequest" + } + } + ], + "tags": [ + "ppanel/admin/coupon" + ] + } + }, + "/v1/admin/coupon/list": { + "get": { + "summary": "Get coupon list", + "operationId": "GetCouponList", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetCouponListResponse" + } + } + } + ] + } + } + }, + "parameters": [ + { + "name": "page", + "in": "query", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "size", + "in": "query", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "subscribe", + "in": "query", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "search", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "ppanel/admin/coupon" + ] + } + }, + "/v1/admin/document/": { + "delete": { + "summary": "Delete document", + "operationId": "DeleteDocument", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DeleteDocumentRequest" + } + } + ], + "tags": [ + "ppanel/admin/document" + ] + }, + "post": { + "summary": "Create document", + "operationId": "CreateDocument", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateDocumentRequest" + } + } + ], + "tags": [ + "ppanel/admin/document" + ] + }, + "put": { + "summary": "Update document", + "operationId": "UpdateDocument", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateDocumentRequest" + } + } + ], + "tags": [ + "ppanel/admin/document" + ] + } + }, + "/v1/admin/document/batch": { + "delete": { + "summary": "Batch delete document", + "operationId": "BatchDeleteDocument", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/BatchDeleteDocumentRequest" + } + } + ], + "tags": [ + "ppanel/admin/document" + ] + } + }, + "/v1/admin/document/detail": { + "get": { + "summary": "Get document detail", + "operationId": "GetDocumentDetail", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/Document" + } + } + } + ] + } + } + }, + "tags": [ + "ppanel/admin/document" + ] + } + }, + "/v1/admin/document/group": { + "delete": { + "summary": "Delete document group", + "operationId": "DeleteDocumentGroup", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DeleteDocumentGroupRequest" + } + } + ], + "tags": [ + "ppanel/admin/document" + ] + }, + "post": { + "summary": "Create document group", + "operationId": "CreateDocumentGroup", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateDocumentGroupRequest" + } + } + ], + "tags": [ + "ppanel/admin/document" + ] + }, + "put": { + "summary": "Update document group", + "operationId": "UpdateDocumentGroup", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateDocumentGroupRequest" + } + } + ], + "tags": [ + "ppanel/admin/document" + ] + } + }, + "/v1/admin/document/group/batch": { + "delete": { + "summary": "Batch delete document group", + "operationId": "BatchDeleteDocumentGroup", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/BatchDeleteDocumentGroupRequest" + } + } + ], + "tags": [ + "ppanel/admin/document" + ] + } + }, + "/v1/admin/document/group/list": { + "get": { + "summary": "Get document group list", + "operationId": "GetDocumentGroupList", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetDocumentGroupListResponse" + } + } + } + ] + } + } + }, + "tags": [ + "ppanel/admin/document" + ] + } + }, + "/v1/admin/document/list": { + "get": { + "summary": "Get document list", + "operationId": "GetDocumentList", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetDocumentListResponse" + } + } + } + ] + } + } + }, + "parameters": [ + { + "name": "page", + "in": "query", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "size", + "in": "query", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "group", + "in": "query", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "search", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "ppanel/admin/document" + ] + } + }, + "/v1/admin/order/": { + "post": { + "summary": "Create order", + "operationId": "CreateOrder", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateOrderRequest" + } + } + ], + "tags": [ + "ppanel/admin/order" + ] + } + }, + "/v1/admin/order/list": { + "get": { + "summary": "Get order list", + "operationId": "GetOrderList", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetOrderListResponse" + } + } + } + ] + } + } + }, + "parameters": [ + { + "name": "page", + "in": "query", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "size", + "in": "query", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "user_id", + "in": "query", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "status", + "in": "query", + "required": false, + "type": "integer", + "format": "uint8" + }, + { + "name": "subscribe_id", + "in": "query", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "search", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "ppanel/admin/order" + ] + } + }, + "/v1/admin/order/status": { + "put": { + "summary": "Update order status", + "operationId": "UpdateOrderStatus", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateOrderStatusRequest" + } + } + ], + "tags": [ + "ppanel/admin/order" + ] + } + }, + "/v1/admin/payment/alipay_f2f": { + "get": { + "summary": "Get alipay f2f payment config", + "operationId": "GetAlipayF2FPaymentConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetAlipayF2FPaymentConfigResponse" + } + } + } + ] + } + } + }, + "tags": [ + "ppanel/admin/payment" + ] + }, + "put": { + "summary": "Update alipay f2f payment config", + "operationId": "UpdateAlipayF2FPaymentConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AlipayF2FPaymentConfig" + } + } + ], + "tags": [ + "ppanel/admin/payment" + ] + } + }, + "/v1/admin/payment/all": { + "get": { + "summary": "Get all payment config", + "operationId": "GetAllPaymentConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetAllPaymentConfigResponse" + } + } + } + ] + } + } + }, + "tags": [ + "ppanel/admin/payment" + ] + } + }, + "/v1/admin/payment/epay": { + "get": { + "summary": "Get epay payment config", + "operationId": "GetEpayPaymentConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetEpayPaymentConfigResponse" + } + } + } + ] + } + } + }, + "tags": [ + "ppanel/admin/payment" + ] + }, + "put": { + "summary": "Update epay payment config", + "operationId": "UpdateEpayPaymentConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/EpayPaymentConfig" + } + } + ], + "tags": [ + "ppanel/admin/payment" + ] + } + }, + "/v1/admin/payment/stripe": { + "get": { + "summary": "Get stripe payment config", + "operationId": "GetStripePaymentConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetStripePaymentConfigResponse" + } + } + } + ] + } + } + }, + "tags": [ + "ppanel/admin/payment" + ] + }, + "put": { + "summary": "Update stripe payment config", + "operationId": "UpdateStripePaymentConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/StripePaymentConfig" + } + } + ], + "tags": [ + "ppanel/admin/payment" + ] + } + }, + "/v1/admin/server/": { + "delete": { + "summary": "Delete node", + "operationId": "DeleteNode", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DeleteNodeRequest" + } + } + ], + "tags": [ + "ppanel/admin/server" + ] + }, + "post": { + "summary": "Create node", + "operationId": "CreateNode", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateNodeRequest" + } + } + ], + "tags": [ + "ppanel/admin/server" + ] + }, + "put": { + "summary": "Update node", + "operationId": "UpdateNode", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateNodeRequest" + } + } + ], + "tags": [ + "ppanel/admin/server" + ] + } + }, + "/v1/admin/server/batch": { + "delete": { + "summary": "Batch delete node", + "operationId": "BatchDeleteNode", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/BatchDeleteRequest" + } + } + ], + "tags": [ + "ppanel/admin/server" + ] + } + }, + "/v1/admin/server/detail": { + "get": { + "summary": "Get node detail", + "operationId": "GetNodeDetail", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/Server" + } + } + } + ] + } + } + }, + "parameters": [ + { + "name": "id", + "in": "query", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "tags": [ + "ppanel/admin/server" + ] + } + }, + "/v1/admin/server/group": { + "delete": { + "summary": "Delete node group", + "operationId": "DeleteNodeGroup", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DeleteNodeGroupRequest" + } + } + ], + "tags": [ + "ppanel/admin/server" + ] + }, + "post": { + "summary": "Create node group", + "operationId": "CreateNodeGroup", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateNodeGroupRequest" + } + } + ], + "tags": [ + "ppanel/admin/server" + ] + }, + "put": { + "summary": "Update node group", + "operationId": "UpdateNodeGroup", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateNodeGroupRequest" + } + } + ], + "tags": [ + "ppanel/admin/server" + ] + } + }, + "/v1/admin/server/group/batch": { + "delete": { + "summary": "Batch delete node group", + "operationId": "BatchDeleteNodeGroup", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/BatchDeleteRequest" + } + } + ], + "tags": [ + "ppanel/admin/server" + ] + } + }, + "/v1/admin/server/group/list": { + "get": { + "summary": "Get node group list", + "operationId": "GetNodeGroupList", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetNodeGroupListResponse" + } + } + } + ] + } + } + }, + "tags": [ + "ppanel/admin/server" + ] + } + }, + "/v1/admin/server/list": { + "get": { + "summary": "Get node list", + "operationId": "GetNodeList", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetNodeServerListResponse" + } + } + } + ] + } + } + }, + "parameters": [ + { + "name": "page", + "in": "query", + "required": true, + "type": "integer", + "format": "int32" + }, + { + "name": "size", + "in": "query", + "required": true, + "type": "integer", + "format": "int32" + }, + { + "name": "group_id", + "in": "query", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "search", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "ppanel/admin/server" + ] + } + }, + "/v1/admin/subscribe/": { + "delete": { + "summary": "Delete subscribe", + "operationId": "DeleteSubscribe", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DeleteSubscribeRequest" + } + } + ], + "tags": [ + "ppanel/admin/subscribe" + ] + }, + "post": { + "summary": "Create subscribe", + "operationId": "CreateSubscribe", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateSubscribeRequest" + } + } + ], + "tags": [ + "ppanel/admin/subscribe" + ] + }, + "put": { + "summary": "Update subscribe", + "operationId": "UpdateSubscribe", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateSubscribeRequest" + } + } + ], + "tags": [ + "ppanel/admin/subscribe" + ] + } + }, + "/v1/admin/subscribe/batch": { + "delete": { + "summary": "Batch delete subscribe", + "operationId": "BatchDeleteSubscribe", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/BatchDeleteSubscribeRequest" + } + } + ], + "tags": [ + "ppanel/admin/subscribe" + ] + } + }, + "/v1/admin/subscribe/details": { + "get": { + "summary": "Get subscribe details", + "operationId": "GetSubscribeDetails", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/Subscribe" + } + } + } + ] + } + } + }, + "parameters": [ + { + "name": "id", + "in": "query", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "tags": [ + "ppanel/admin/subscribe" + ] + } + }, + "/v1/admin/subscribe/group": { + "delete": { + "summary": "Delete subscribe group", + "operationId": "DeleteSubscribeGroup", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DeleteSubscribeGroupRequest" + } + } + ], + "tags": [ + "ppanel/admin/subscribe" + ] + }, + "post": { + "summary": "Create subscribe group", + "operationId": "CreateSubscribeGroup", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateSubscribeGroupRequest" + } + } + ], + "tags": [ + "ppanel/admin/subscribe" + ] + }, + "put": { + "summary": "Update subscribe group", + "operationId": "UpdateSubscribeGroup", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateSubscribeGroupRequest" + } + } + ], + "tags": [ + "ppanel/admin/subscribe" + ] + } + }, + "/v1/admin/subscribe/group/batch": { + "delete": { + "summary": "Batch delete subscribe group", + "operationId": "BatchDeleteSubscribeGroup", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/BatchDeleteSubscribeGroupRequest" + } + } + ], + "tags": [ + "ppanel/admin/subscribe" + ] + } + }, + "/v1/admin/subscribe/group/list": { + "get": { + "summary": "Get subscribe group list", + "operationId": "GetSubscribeGroupList", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetSubscribeGroupListResponse" + } + } + } + ] + } + } + }, + "tags": [ + "ppanel/admin/subscribe" + ] + } + }, + "/v1/admin/subscribe/list": { + "get": { + "summary": "Get subscribe list", + "operationId": "GetSubscribeList", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetSubscribeListResponse" + } + } + } + ] + } + } + }, + "parameters": [ + { + "name": "page", + "in": "query", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "size", + "in": "query", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "group_id", + "in": "query", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "search", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "ppanel/admin/subscribe" + ] + } + }, + "/v1/admin/system/application": { + "get": { + "summary": "Get application", + "operationId": "GetApplication", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetApplicationResponse" + } + } + } + ] + } + } + }, + "tags": [ + "ppanel/admin/system" + ] + }, + "delete": { + "summary": "Delete application", + "operationId": "DeleteApplication", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "description": "Delete application request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DeleteApplicationRequest" + } + } + ], + "tags": [ + "ppanel/admin/system" + ] + }, + "post": { + "summary": "Create application", + "operationId": "CreateApplication", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "description": "Create application request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateApplicationRequest" + } + } + ], + "tags": [ + "ppanel/admin/system" + ] + }, + "put": { + "summary": "Update application", + "operationId": "UpdateApplication", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "description": "Update application request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateApplicationRequest" + } + } + ], + "tags": [ + "ppanel/admin/system" + ] + } + }, + "/v1/admin/system/email_config": { + "get": { + "summary": "Get email smtp config", + "operationId": "GetEmailSmtpConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetEmailSmtpConfigResponse" + } + } + } + ] + } + } + }, + "tags": [ + "ppanel/admin/system" + ] + }, + "put": { + "summary": "Update email smtp config", + "operationId": "UpdateEmailSmtpConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "description": "Update email smtp config request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateEmailSmtpConfigRequest" + } + } + ], + "tags": [ + "ppanel/admin/system" + ] + } + }, + "/v1/admin/system/invite_config": { + "get": { + "summary": "Get invite config", + "operationId": "GetInviteConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetInviteConfigResponse" + } + } + } + ] + } + } + }, + "tags": [ + "ppanel/admin/system" + ] + }, + "put": { + "summary": "Update invite config", + "operationId": "UpdateInviteConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "description": "Update invite config request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateInviteConfigRequest" + } + } + ], + "tags": [ + "ppanel/admin/system" + ] + } + }, + "/v1/admin/system/node_config": { + "get": { + "summary": "Get node config", + "operationId": "GetNodeConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetNodeConfigResponse" + } + } + } + ] + } + } + }, + "tags": [ + "ppanel/admin/system" + ] + }, + "put": { + "summary": "Update node config", + "operationId": "UpdateNodeConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "description": "Update node config request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateNodeConfigRequest" + } + } + ], + "tags": [ + "ppanel/admin/system" + ] + } + }, + "/v1/admin/system/register_config": { + "get": { + "summary": "Get register config", + "operationId": "GetRegisterConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetRegisterConfigResponse" + } + } + } + ] + } + } + }, + "tags": [ + "ppanel/admin/system" + ] + }, + "put": { + "summary": "Update register config", + "operationId": "UpdateRegisterConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "description": "Update register config request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateRegisterConfigRequest" + } + } + ], + "tags": [ + "ppanel/admin/system" + ] + } + }, + "/v1/admin/system/site_config": { + "get": { + "summary": "Get site config", + "operationId": "GetSiteConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetSiteConfigResponse" + } + } + } + ] + } + } + }, + "tags": [ + "ppanel/admin/system" + ] + }, + "put": { + "summary": "Update site config", + "operationId": "UpdateSiteConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "description": "Update site config request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateSiteConfigRequest" + } + } + ], + "tags": [ + "ppanel/admin/system" + ] + } + }, + "/v1/admin/system/subscribe_config": { + "get": { + "summary": "Get subscribe config", + "operationId": "GetSubscribeConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetSubscribeConfigResponse" + } + } + } + ] + } + } + }, + "tags": [ + "ppanel/admin/system" + ] + }, + "put": { + "summary": "Update subscribe config", + "operationId": "UpdateSubscribeConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "description": "Update subscribe config request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateSubscribeConfigRequest" + } + } + ], + "tags": [ + "ppanel/admin/system" + ] + } + }, + "/v1/admin/system/subscribe_type": { + "get": { + "summary": "Get subscribe type", + "operationId": "GetSubscribeType", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetSubscribeTypeResponse" + } + } + } + ] + } + } + }, + "tags": [ + "ppanel/admin/system" + ] + } + }, + "/v1/admin/system/telegram_config": { + "get": { + "summary": "Get Telegram Config", + "operationId": "GetTelegramConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetTelegramConfigResponse" + } + } + } + ] + } + } + }, + "tags": [ + "ppanel/admin/system" + ] + }, + "put": { + "summary": "Update Telegram Config", + "operationId": "UpdateTelegramConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "description": "Update Telegram config request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateTelegramConfigRequest" + } + } + ], + "tags": [ + "ppanel/admin/system" + ] + } + }, + "/v1/admin/system/test_email": { + "post": { + "summary": "Test email smtp", + "operationId": "TestEmailSmtp", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "description": "Test email smtp request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/TestEmailSmtpRequest" + } + } + ], + "tags": [ + "ppanel/admin/system" + ] + } + }, + "/v1/admin/system/tos_config": { + "get": { + "summary": "Get Team of Service Config", + "operationId": "GetTosConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetTosConfigResponse" + } + } + } + ] + } + } + }, + "tags": [ + "ppanel/admin/system" + ] + }, + "put": { + "summary": "Update Team of Service Config", + "operationId": "UpdateTosConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "description": "Update Team of Service config request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateTosConfigRequest" + } + } + ], + "tags": [ + "ppanel/admin/system" + ] + } + }, + "/v1/admin/system/verify_config": { + "get": { + "summary": "Get verify config", + "operationId": "GetVerifyConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetVerifyConfigResponse" + } + } + } + ] + } + } + }, + "tags": [ + "ppanel/admin/system" + ] + }, + "put": { + "summary": "Update verify config", + "operationId": "UpdateVerifyConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "description": "Update verify config request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateVerifyConfigRequest" + } + } + ], + "tags": [ + "ppanel/admin/system" + ] + } + }, + "/v1/admin/ticket/detail": { + "get": { + "summary": "Get ticket detail", + "operationId": "GetTicket", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/Ticket" + } + } + } + ] + } + } + }, + "parameters": [ + { + "name": "id", + "in": "query", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "tags": [ + "ppanel/admin/ticket" + ] + } + }, + "/v1/admin/ticket/follow": { + "post": { + "summary": "Create ticket follow", + "operationId": "CreateTicketFollow", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateTicketFollowRequest" + } + } + ], + "tags": [ + "ppanel/admin/ticket" + ] + } + }, + "/v1/admin/ticket/list": { + "get": { + "summary": "Get ticket list", + "operationId": "GetTicketList", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetTicketListResponse" + } + } + } + ] + } + } + }, + "parameters": [ + { + "name": "page", + "in": "query", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "size", + "in": "query", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "user_id", + "in": "query", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "status", + "in": "query", + "required": false, + "type": "integer", + "format": "uint8" + }, + { + "name": "search", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "ppanel/admin/ticket" + ] + } + }, + "/v1/admin/user/": { + "delete": { + "summary": "Delete user", + "operationId": "DeleteUser", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "id", + "in": "query", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "tags": [ + "ppanel/admin/user" + ], + "security": [ + { + "apiKey": [] + } + ] + }, + "post": { + "summary": "Create user", + "operationId": "CreateUser", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateUserRequest" + } + } + ], + "tags": [ + "ppanel/admin/user" + ], + "security": [ + { + "apiKey": [] + } + ] + }, + "put": { + "summary": "Update user", + "operationId": "UpdateUser", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateUserRequest" + } + } + ], + "tags": [ + "ppanel/admin/user" + ], + "security": [ + { + "apiKey": [] + } + ] + } + }, + "/v1/admin/user/batch": { + "delete": { + "summary": "Batch delete user", + "operationId": "BatchDeleteUser", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": {} + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/BatchDeleteRequest" + } + } + ], + "tags": [ + "ppanel/admin/user" + ], + "security": [ + { + "apiKey": [] + } + ] + } + }, + "/v1/admin/user/current": { + "get": { + "summary": "Current user", + "operationId": "CurrentUser", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/User" + } + } + } + ] + } + } + }, + "tags": [ + "ppanel/admin/user" + ], + "security": [ + { + "apiKey": [] + } + ] + } + }, + "/v1/admin/user/detail": { + "get": { + "summary": "Get user detail", + "operationId": "GetUserDetail", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/User" + } + } + } + ] + } + } + }, + "parameters": [ + { + "name": "id", + "in": "query", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "tags": [ + "ppanel/admin/user" + ], + "security": [ + { + "apiKey": [] + } + ] + } + }, + "/v1/admin/user/list": { + "get": { + "summary": "Get user list", + "operationId": "GetUserList", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetUserListResponse" + } + } + } + ] + } + } + }, + "parameters": [ + { + "name": "page", + "in": "query", + "required": true, + "type": "integer", + "format": "int32" + }, + { + "name": "size", + "in": "query", + "required": true, + "type": "integer", + "format": "int32" + } + ], + "tags": [ + "ppanel/admin/user" + ], + "security": [ + { + "apiKey": [] + } + ] + } + }, + "/v1/auth/check": { + "get": { + "summary": "Check user is exist", + "operationId": "CheckUser", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/CheckUserResponse" + } + } + } + ] + } + } + }, + "parameters": [ + { + "name": "email", + "in": "query", + "required": true, + "type": "string" + } + ], + "tags": [ + "ppanel/auth" + ] + } + }, + "/v1/auth/login": { + "post": { + "summary": "User login", + "operationId": "UserLogin", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/LoginResponse" + } + } + } + ] + } + } + }, + "parameters": [ + { + "name": "X-Original-Forwarded-For", + "in": "header", + "required": true, + "type": "string" + }, + { + "name": "body", + "description": "User login request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UserLoginRequest" + } + } + ], + "tags": [ + "ppanel/auth" + ] + } + }, + "/v1/auth/register": { + "post": { + "summary": "User register", + "operationId": "UserRegister", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/LoginResponse" + } + } + } + ] + } + } + }, + "parameters": [ + { + "name": "X-Original-Forwarded-For", + "in": "header", + "required": true, + "type": "string" + }, + { + "name": "body", + "description": "User login response", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UserRegisterRequest" + } + } + ], + "tags": [ + "ppanel/auth" + ] + } + }, + "/v1/auth/reset": { + "post": { + "summary": "Reset password", + "operationId": "ResetPassword", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/LoginResponse" + } + } + } + ] + } + } + }, + "parameters": [ + { + "name": "X-Original-Forwarded-For", + "in": "header", + "required": true, + "type": "string" + }, + { + "name": "body", + "description": "User login response", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ResetPasswordRequest" + } + } + ], + "tags": [ + "ppanel/auth" + ] + } + }, + "/v1/common/site/config": { + "get": { + "summary": "Get site config", + "operationId": "SiteConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetSiteConfigResponse" + } + } + } + ] + } + } + }, + "tags": [ + "ppanel/common" + ] + } + }, + "/v1/user/common/get_code": { + "post": { + "summary": "Get verification code", + "operationId": "GetCode", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/GetCodeResponse" + } + } + } + ] + } + } + }, + "parameters": [ + { + "name": "body", + "description": "GetCodeRequest Get code request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/GetCodeRequest" + } + } + ], + "tags": [ + "ppanel/user/common" + ] + } + } + }, + "definitions": { + "AlipayF2FConfig": { + "type": "object", + "properties": { + "app_id": { + "type": "string" + }, + "private_key": { + "type": "string" + }, + "public_key": { + "type": "string" + }, + "invoice_name": { + "type": "string" + } + }, + "title": "AlipayF2FConfig", + "required": [ + "app_id", + "private_key", + "public_key", + "invoice_name" + ] + }, + "AlipayF2FPaymentConfig": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "config": { + "$ref": "#/definitions/AlipayF2FConfig" + }, + "fee_mode": { + "type": "integer", + "format": "uint32" + }, + "fee_percent": { + "type": "integer", + "format": "int64" + }, + "fee_amount": { + "type": "integer", + "format": "int64" + }, + "enable": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "AlipayF2FPaymentConfig", + "required": [ + "id", + "name", + "config", + "fee_mode", + "enable" + ] + }, + "Announcement": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "title": { + "type": "string" + }, + "content": { + "type": "string" + }, + "enable": { + "type": "boolean", + "format": "boolean" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "updated_at": { + "type": "integer", + "format": "int64" + } + }, + "title": "Announcement", + "required": [ + "id", + "title", + "content", + "enable", + "created_at", + "updated_at" + ] + }, + "Application": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "subscribe_type": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "title": "Application", + "required": [ + "id", + "name", + "platform", + "subscribe_type", + "icon", + "url" + ] + }, + "BatchDeleteCouponRequest": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + }, + "title": "BatchDeleteCouponRequest", + "required": [ + "ids" + ] + }, + "BatchDeleteDocumentGroupRequest": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + }, + "title": "BatchDeleteDocumentGroupRequest", + "required": [ + "ids" + ] + }, + "BatchDeleteDocumentRequest": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + }, + "title": "BatchDeleteDocumentRequest", + "required": [ + "ids" + ] + }, + "BatchDeleteRequest": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + }, + "title": "BatchDeleteRequest", + "required": [ + "ids" + ] + }, + "BatchDeleteSubscribeGroupRequest": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + }, + "title": "BatchDeleteSubscribeGroupRequest", + "required": [ + "ids" + ] + }, + "BatchDeleteSubscribeRequest": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + }, + "title": "BatchDeleteSubscribeRequest", + "required": [ + "ids" + ] + }, + "CheckUserRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + } + }, + "title": "CheckUserRequest", + "required": [ + "email" + ] + }, + "CheckUserResponse": { + "type": "object", + "properties": { + "exist": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "CheckUserResponse", + "required": [ + "exist" + ] + }, + "Coupon": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "code": { + "type": "string" + }, + "count": { + "type": "integer", + "format": "int64" + }, + "type": { + "type": "integer", + "format": "uint8" + }, + "discount": { + "type": "integer", + "format": "int64" + }, + "start_time": { + "type": "integer", + "format": "int64" + }, + "expire_time": { + "type": "integer", + "format": "int64" + }, + "user_limit": { + "type": "integer", + "format": "int64" + }, + "subscribe": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "used_count": { + "type": "integer", + "format": "int64" + }, + "enable": { + "type": "boolean", + "format": "boolean" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "updated_at": { + "type": "integer", + "format": "int64" + } + }, + "title": "Coupon", + "required": [ + "id", + "name", + "code", + "count", + "type", + "discount", + "start_time", + "expire_time", + "user_limit", + "subscribe", + "used_count", + "enable", + "created_at", + "updated_at" + ] + }, + "CreateAnnouncementRequest": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "content": { + "type": "string" + } + }, + "title": "CreateAnnouncementRequest", + "required": [ + "title", + "content" + ] + }, + "CreateApplicationRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "platform": { + "type": "string", + "enum": [ + "windows", + "mac", + "linux", + "android", + "ios" + ] + }, + "subscribe_type": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "title": "CreateApplicationRequest", + "required": [ + "name", + "platform", + "subscribe_type", + "icon", + "url" + ] + }, + "CreateCouponRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "code": { + "type": "string" + }, + "count": { + "type": "integer", + "format": "int64" + }, + "type": { + "type": "integer", + "format": "uint8" + }, + "discount": { + "type": "integer", + "format": "int64" + }, + "start_time": { + "type": "integer", + "format": "int64" + }, + "expire_time": { + "type": "integer", + "format": "int64" + }, + "user_limit": { + "type": "integer", + "format": "int64" + }, + "subscribe": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "used_count": { + "type": "integer", + "format": "int64" + }, + "enable": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "CreateCouponRequest", + "required": [ + "name", + "type", + "discount", + "start_time", + "expire_time" + ] + }, + "CreateDocumentGroupRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "title": "CreateDocumentGroupRequest", + "required": [ + "name", + "description" + ] + }, + "CreateDocumentRequest": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "content": { + "type": "string" + }, + "group_id": { + "type": "integer", + "format": "int64" + }, + "show": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "CreateDocumentRequest", + "required": [ + "title", + "content", + "group_id", + "show" + ] + }, + "CreateNodeGroupRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "title": "CreateNodeGroupRequest", + "required": [ + "name", + "description" + ] + }, + "CreateNodeRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "server_addr": { + "type": "string" + }, + "speed_limit": { + "type": "integer", + "format": "int32" + }, + "traffic_ratio": { + "type": "number", + "format": "float" + }, + "groupId": { + "type": "integer", + "format": "int64" + }, + "protocol": { + "type": "string" + }, + "enable": { + "type": "boolean", + "format": "boolean" + }, + "vmess": { + "$ref": "#/definitions/Vmess" + }, + "vless": { + "$ref": "#/definitions/Vless" + }, + "trojan": { + "$ref": "#/definitions/Trojan" + }, + "shadowsocks": { + "$ref": "#/definitions/Shadowsocks" + }, + "enable_relay": { + "type": "boolean", + "format": "boolean" + }, + "relay_host": { + "type": "string" + }, + "relay_port": { + "type": "integer", + "format": "int32" + } + }, + "title": "CreateNodeRequest", + "required": [ + "name", + "server_addr", + "speed_limit", + "traffic_ratio", + "groupId", + "protocol", + "enable", + "enable_relay" + ] + }, + "CreateOrderRequest": { + "type": "object", + "properties": { + "user_id": { + "type": "integer", + "format": "int64" + }, + "type": { + "type": "integer", + "format": "uint8" + }, + "price": { + "type": "integer", + "format": "int64" + }, + "amount": { + "type": "integer", + "format": "int64" + }, + "fee_amount": { + "type": "integer", + "format": "int64" + }, + "coupon": { + "type": "string" + }, + "reduction": { + "type": "integer", + "format": "int64" + }, + "method": { + "type": "string" + }, + "trade_no": { + "type": "string" + }, + "status": { + "type": "integer", + "format": "uint8" + }, + "subscribe_id": { + "type": "integer", + "format": "int64" + } + }, + "title": "CreateOrderRequest", + "required": [ + "user_id", + "type", + "price", + "amount", + "fee_amount" + ] + }, + "CreateSubscribeGroupRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "title": "CreateSubscribeGroupRequest", + "required": [ + "name", + "description" + ] + }, + "CreateSubscribeRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "unit_price": { + "type": "integer", + "format": "int64" + }, + "discount": { + "type": "array", + "items": { + "$ref": "#/definitions/SubscribeDiscount" + } + }, + "replacement": { + "type": "integer", + "format": "int64" + }, + "inventory": { + "type": "integer", + "format": "int64" + }, + "traffic": { + "type": "integer", + "format": "int64" + }, + "speed_limit": { + "type": "integer", + "format": "int64" + }, + "device_limit": { + "type": "integer", + "format": "int64" + }, + "quota": { + "type": "integer", + "format": "int64" + }, + "group_id": { + "type": "integer", + "format": "int64" + }, + "server_group": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "server": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "show": { + "type": "boolean", + "format": "boolean" + }, + "sell": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "CreateSubscribeRequest", + "required": [ + "name", + "description", + "unit_price", + "discount", + "replacement", + "inventory", + "traffic", + "speed_limit", + "device_limit", + "quota", + "group_id", + "server_group", + "server", + "show", + "sell" + ] + }, + "CreateTicketFollowRequest": { + "type": "object", + "properties": { + "ticket_id": { + "type": "integer", + "format": "int64" + }, + "from": { + "type": "string" + }, + "type": { + "type": "integer", + "format": "uint8" + }, + "content": { + "type": "string" + } + }, + "title": "CreateTicketFollowRequest", + "required": [ + "ticket_id", + "from", + "type", + "content" + ] + }, + "CreateUserRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "product_id": { + "type": "integer", + "format": "int64" + }, + "duration": { + "type": "integer", + "format": "int64" + }, + "referer_user": { + "type": "string" + }, + "refer_code": { + "type": "string" + }, + "balance": { + "type": "integer", + "format": "int64" + }, + "is_admin": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "CreateUserRequest", + "required": [ + "email", + "password", + "product_id", + "duration", + "referer_user", + "refer_code", + "balance", + "is_admin" + ] + }, + "DeleteAnnouncementRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + } + }, + "title": "DeleteAnnouncementRequest", + "required": [ + "id" + ] + }, + "DeleteApplicationRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + } + }, + "title": "DeleteApplicationRequest", + "required": [ + "id" + ] + }, + "DeleteCouponRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + } + }, + "title": "DeleteCouponRequest", + "required": [ + "id" + ] + }, + "DeleteDocumentGroupRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + } + }, + "title": "DeleteDocumentGroupRequest", + "required": [ + "id" + ] + }, + "DeleteDocumentRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + } + }, + "title": "DeleteDocumentRequest", + "required": [ + "id" + ] + }, + "DeleteNodeGroupRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + } + }, + "title": "DeleteNodeGroupRequest", + "required": [ + "id" + ] + }, + "DeleteNodeRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + } + }, + "title": "DeleteNodeRequest", + "required": [ + "id" + ] + }, + "DeleteSubscribeGroupRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + } + }, + "title": "DeleteSubscribeGroupRequest", + "required": [ + "id" + ] + }, + "DeleteSubscribeRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + } + }, + "title": "DeleteSubscribeRequest", + "required": [ + "id" + ] + }, + "Document": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "title": { + "type": "string" + }, + "content": { + "type": "string" + }, + "group_id": { + "$ref": "#/definitions/DocumentGroup" + }, + "show": { + "type": "boolean", + "format": "boolean" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "updated_at": { + "type": "integer", + "format": "int64" + } + }, + "title": "Document", + "required": [ + "id", + "title", + "content", + "group_id", + "show", + "created_at", + "updated_at" + ] + }, + "DocumentGroup": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "updated_at": { + "type": "integer", + "format": "int64" + } + }, + "title": "DocumentGroup", + "required": [ + "id", + "name", + "description", + "created_at", + "updated_at" + ] + }, + "EpayConfig": { + "type": "object", + "properties": { + "pid": { + "type": "string" + }, + "url": { + "type": "string" + }, + "key": { + "type": "string" + } + }, + "title": "EpayConfig", + "required": [ + "pid", + "url", + "key" + ] + }, + "EpayPaymentConfig": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "config": { + "$ref": "#/definitions/EpayConfig" + }, + "fee_mode": { + "type": "integer", + "format": "uint32" + }, + "fee_percent": { + "type": "integer", + "format": "int64" + }, + "fee_amount": { + "type": "integer", + "format": "int64" + }, + "enable": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "EpayPaymentConfig", + "required": [ + "id", + "name", + "config", + "fee_mode", + "enable" + ] + }, + "Follow": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "ticket_id": { + "type": "integer", + "format": "int64" + }, + "from": { + "type": "string" + }, + "type": { + "type": "integer", + "format": "uint8" + }, + "content": { + "type": "string" + }, + "created_at": { + "type": "integer", + "format": "int64" + } + }, + "title": "Follow", + "required": [ + "id", + "ticket_id", + "from", + "type", + "content", + "created_at" + ] + }, + "GetAlipayF2FPaymentConfigResponse": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "config": { + "$ref": "#/definitions/AlipayF2FConfig" + }, + "fee_mode": { + "type": "integer", + "format": "uint32" + }, + "fee_percent": { + "type": "integer", + "format": "int64" + }, + "fee_amount": { + "type": "integer", + "format": "int64" + }, + "enable": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "GetAlipayF2FPaymentConfigResponse", + "required": [ + "id", + "name", + "icon", + "domain", + "config", + "fee_mode", + "fee_percent", + "fee_amount", + "enable" + ] + }, + "GetAllPaymentConfigResponse": { + "type": "object", + "properties": { + "stripe": { + "$ref": "#/definitions/GetStripePaymentConfigResponse" + }, + "alipay": { + "$ref": "#/definitions/GetAlipayF2FPaymentConfigResponse" + }, + "epay": { + "$ref": "#/definitions/GetEpayPaymentConfigResponse" + } + }, + "title": "GetAllPaymentConfigResponse", + "required": [ + "stripe", + "alipay", + "epay" + ] + }, + "GetAnnouncementListRequest": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "format": "int64" + }, + "size": { + "type": "integer", + "format": "int64" + }, + "enable": { + "type": "boolean", + "format": "boolean" + }, + "search": { + "type": "string" + } + }, + "title": "GetAnnouncementListRequest", + "required": [ + "page", + "size" + ] + }, + "GetAnnouncementListResponse": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/Announcement" + } + } + }, + "title": "GetAnnouncementListResponse", + "required": [ + "total", + "list" + ] + }, + "GetAnnouncementRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + } + }, + "title": "GetAnnouncementRequest", + "required": [ + "id" + ] + }, + "GetApplicationResponse": { + "type": "object", + "properties": { + "windows": { + "type": "array", + "items": { + "$ref": "#/definitions/Application" + } + }, + "mac": { + "type": "array", + "items": { + "$ref": "#/definitions/Application" + } + }, + "linux": { + "type": "array", + "items": { + "$ref": "#/definitions/Application" + } + }, + "android": { + "type": "array", + "items": { + "$ref": "#/definitions/Application" + } + }, + "ios": { + "type": "array", + "items": { + "$ref": "#/definitions/Application" + } + } + }, + "title": "GetApplicationResponse", + "required": [ + "windows", + "mac", + "linux", + "android", + "ios" + ] + }, + "GetCodeRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "type": { + "type": "integer", + "format": "uint8" + } + }, + "title": "GetCodeRequest", + "required": [ + "email", + "type" + ] + }, + "GetCodeResponse": { + "type": "object", + "properties": { + "status": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "GetCodeResponse", + "required": [ + "status" + ] + }, + "GetCouponListRequest": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "format": "int64" + }, + "size": { + "type": "integer", + "format": "int64" + }, + "subscribe": { + "type": "integer", + "format": "int64" + }, + "search": { + "type": "string" + } + }, + "title": "GetCouponListRequest", + "required": [ + "page", + "size" + ] + }, + "GetCouponListResponse": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/Coupon" + } + } + }, + "title": "GetCouponListResponse", + "required": [ + "total", + "list" + ] + }, + "GetDetailRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + } + }, + "title": "GetDetailRequest", + "required": [ + "id" + ] + }, + "GetDocumentDetailRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + } + }, + "title": "GetDocumentDetailRequest", + "required": [ + "id" + ] + }, + "GetDocumentGroupListResponse": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/DocumentGroup" + } + } + }, + "title": "GetDocumentGroupListResponse", + "required": [ + "total", + "list" + ] + }, + "GetDocumentListRequest": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "format": "int64" + }, + "size": { + "type": "integer", + "format": "int64" + }, + "group": { + "type": "integer", + "format": "int64" + }, + "search": { + "type": "string" + } + }, + "title": "GetDocumentListRequest", + "required": [ + "page", + "size" + ] + }, + "GetDocumentListResponse": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/Document" + } + } + }, + "title": "GetDocumentListResponse", + "required": [ + "total", + "list" + ] + }, + "GetEmailSmtpConfigResponse": { + "type": "object", + "properties": { + "email_smtp_host": { + "type": "string" + }, + "email_smtp_port": { + "type": "integer", + "format": "int64" + }, + "email_smtp_user": { + "type": "string" + }, + "email_smtp_pass": { + "type": "string" + }, + "email_smtp_from": { + "type": "string" + }, + "email_smtp_ssl": { + "type": "boolean", + "format": "boolean" + }, + "email_template": { + "type": "string" + } + }, + "title": "GetEmailSmtpConfigResponse", + "required": [ + "email_smtp_host", + "email_smtp_port", + "email_smtp_user", + "email_smtp_pass", + "email_smtp_from", + "email_smtp_ssl", + "email_template" + ] + }, + "GetEpayPaymentConfigResponse": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "config": { + "$ref": "#/definitions/EpayConfig" + }, + "fee_mode": { + "type": "integer", + "format": "uint32" + }, + "fee_percent": { + "type": "integer", + "format": "int64" + }, + "fee_amount": { + "type": "integer", + "format": "int64" + }, + "enable": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "GetEpayPaymentConfigResponse", + "required": [ + "id", + "name", + "icon", + "domain", + "config", + "fee_mode", + "fee_percent", + "fee_amount", + "enable" + ] + }, + "GetInviteConfigResponse": { + "type": "object", + "properties": { + "forced_invite": { + "type": "boolean", + "format": "boolean" + }, + "referral_percentage": { + "type": "integer", + "format": "int64" + }, + "only_first_purchase": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "GetInviteConfigResponse", + "required": [ + "forced_invite", + "referral_percentage", + "only_first_purchase" + ] + }, + "GetListByPageRequest": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "format": "int32" + }, + "size": { + "type": "integer", + "format": "int32" + } + }, + "title": "GetListByPageRequest", + "required": [ + "page", + "size" + ] + }, + "GetNodeConfigResponse": { + "type": "object", + "properties": { + "node_secret": { + "type": "string" + }, + "node_pull_interval": { + "type": "integer", + "format": "int64" + }, + "node_push_interval": { + "type": "integer", + "format": "int64" + } + }, + "title": "GetNodeConfigResponse", + "required": [ + "node_secret", + "node_pull_interval", + "node_push_interval" + ] + }, + "GetNodeGroupListResponse": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/ServerGroup" + } + } + }, + "title": "GetNodeGroupListResponse", + "required": [ + "total", + "list" + ] + }, + "GetNodeServerListRequest": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "format": "int32" + }, + "size": { + "type": "integer", + "format": "int32" + }, + "group_id": { + "type": "integer", + "format": "int64" + }, + "search": { + "type": "string" + } + }, + "title": "GetNodeServerListRequest", + "required": [ + "page", + "size" + ] + }, + "GetNodeServerListResponse": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/Server" + } + } + }, + "title": "GetNodeServerListResponse", + "required": [ + "total", + "list" + ] + }, + "GetOrderListRequest": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "format": "int64" + }, + "size": { + "type": "integer", + "format": "int64" + }, + "user_id": { + "type": "integer", + "format": "int64" + }, + "status": { + "type": "integer", + "format": "uint8" + }, + "subscribe_id": { + "type": "integer", + "format": "int64" + }, + "search": { + "type": "string" + } + }, + "title": "GetOrderListRequest", + "required": [ + "page", + "size" + ] + }, + "GetOrderListResponse": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/Order" + } + } + }, + "title": "GetOrderListResponse", + "required": [ + "total", + "list" + ] + }, + "GetRegisterConfigResponse": { + "type": "object", + "properties": { + "stop_register": { + "type": "boolean", + "format": "boolean" + }, + "enable_trial": { + "type": "boolean", + "format": "boolean" + }, + "enable_email_verify": { + "type": "boolean", + "format": "boolean" + }, + "enable_email_domain_suffix": { + "type": "boolean", + "format": "boolean" + }, + "email_domain_suffix_list": { + "type": "string" + }, + "enable_ip_register_limit": { + "type": "boolean", + "format": "boolean" + }, + "ip_register_limit": { + "type": "integer", + "format": "int64" + }, + "ip_register_limit_duration": { + "type": "integer", + "format": "int64" + } + }, + "title": "GetRegisterConfigResponse", + "required": [ + "stop_register", + "enable_trial", + "enable_email_verify", + "enable_email_domain_suffix", + "email_domain_suffix_list", + "enable_ip_register_limit", + "ip_register_limit", + "ip_register_limit_duration" + ] + }, + "GetSiteConfigResponse": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "site_name": { + "type": "string" + }, + "site_desc": { + "type": "string" + }, + "site_logo": { + "type": "string" + } + }, + "title": "GetSiteConfigResponse", + "required": [ + "host", + "site_name", + "site_desc", + "site_logo" + ] + }, + "GetStripePaymentConfigResponse": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "config": { + "$ref": "#/definitions/StripeConfig" + }, + "fee_mode": { + "type": "integer", + "format": "uint32" + }, + "fee_percent": { + "type": "integer", + "format": "int64" + }, + "fee_amount": { + "type": "integer", + "format": "int64" + }, + "enable": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "GetStripePaymentConfigResponse", + "required": [ + "id", + "name", + "icon", + "domain", + "config", + "fee_mode", + "fee_percent", + "fee_amount", + "enable" + ] + }, + "GetSubscribeConfigResponse": { + "type": "object", + "properties": { + "single_model": { + "type": "boolean", + "format": "boolean" + }, + "subscribe_path": { + "type": "string" + }, + "subscribe_domain": { + "type": "string" + }, + "pan_domain": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "GetSubscribeConfigResponse", + "required": [ + "single_model", + "subscribe_path", + "subscribe_domain", + "pan_domain" + ] + }, + "GetSubscribeGroupListResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/SubscribeGroup" + } + }, + "total": { + "type": "integer", + "format": "int64" + } + }, + "title": "GetSubscribeGroupListResponse", + "required": [ + "list", + "total" + ] + }, + "GetSubscribeListRequest": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "format": "int64" + }, + "size": { + "type": "integer", + "format": "int64" + }, + "group_id": { + "type": "integer", + "format": "int64" + }, + "search": { + "type": "string" + } + }, + "title": "GetSubscribeListRequest", + "required": [ + "page", + "size" + ] + }, + "GetSubscribeListResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/Subscribe" + } + }, + "total": { + "type": "integer", + "format": "int64" + } + }, + "title": "GetSubscribeListResponse", + "required": [ + "list", + "total" + ] + }, + "GetSubscribeTypeResponse": { + "type": "object", + "properties": { + "subscribe_types": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "title": "GetSubscribeTypeResponse", + "required": [ + "subscribe_types" + ] + }, + "GetTelegramConfigResponse": { + "type": "object", + "properties": { + "telegram_bot_token": { + "type": "string" + }, + "telegram_group_url": { + "type": "string" + }, + "telegram_notify": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "GetTelegramConfigResponse", + "required": [ + "telegram_bot_token", + "telegram_group_url", + "telegram_notify" + ] + }, + "GetTicketListRequest": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "format": "int64" + }, + "size": { + "type": "integer", + "format": "int64" + }, + "user_id": { + "type": "integer", + "format": "int64" + }, + "status": { + "type": "integer", + "format": "uint8" + }, + "search": { + "type": "string" + } + }, + "title": "GetTicketListRequest", + "required": [ + "page", + "size" + ] + }, + "GetTicketListResponse": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/Ticket" + } + } + }, + "title": "GetTicketListResponse", + "required": [ + "total", + "list" + ] + }, + "GetTicketRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + } + }, + "title": "GetTicketRequest", + "required": [ + "id" + ] + }, + "GetTosConfigResponse": { + "type": "object", + "properties": { + "tos_content": { + "type": "string" + } + }, + "title": "GetTosConfigResponse", + "required": [ + "tos_content" + ] + }, + "GetUserListResponse": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/User" + } + } + }, + "title": "GetUserListResponse", + "required": [ + "total", + "list" + ] + }, + "GetVerifyConfigResponse": { + "type": "object", + "properties": { + "turnstile_site_key": { + "type": "string" + }, + "turnstile_secret": { + "type": "string" + }, + "enable_login_verify": { + "type": "boolean", + "format": "boolean" + }, + "enable_register_verify": { + "type": "boolean", + "format": "boolean" + }, + "enable_reset_password_verify": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "GetVerifyConfigResponse", + "required": [ + "turnstile_site_key", + "turnstile_secret", + "enable_login_verify", + "enable_register_verify", + "enable_reset_password_verify" + ] + }, + "LoginResponse": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + }, + "title": "LoginResponse", + "required": [ + "token" + ] + }, + "Order": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "user_id": { + "type": "integer", + "format": "int64" + }, + "order_no": { + "type": "string" + }, + "type": { + "type": "integer", + "format": "uint8" + }, + "price": { + "type": "integer", + "format": "int64" + }, + "amount": { + "type": "integer", + "format": "int64" + }, + "fee_amount": { + "type": "integer", + "format": "int64" + }, + "coupon": { + "type": "string" + }, + "reduction": { + "type": "integer", + "format": "int64" + }, + "method": { + "type": "string" + }, + "trade_no": { + "type": "string" + }, + "status": { + "type": "integer", + "format": "uint8" + }, + "subscribe_id": { + "type": "integer", + "format": "int64" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "updated_at": { + "type": "integer", + "format": "int64" + } + }, + "title": "Order", + "required": [ + "id", + "user_id", + "order_no", + "type", + "price", + "amount", + "fee_amount", + "coupon", + "reduction", + "method", + "trade_no", + "status", + "subscribe_id", + "created_at", + "updated_at" + ] + }, + "ResetPasswordRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "code": { + "type": "string" + }, + "cf_token": { + "type": "string" + } + }, + "title": "ResetPasswordRequest", + "required": [ + "email", + "password" + ] + }, + "Response": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码" + }, + "msg": { + "type": "string", + "description": "消息" + }, + "data": { + "type": "object", + "description": "数据" + } + } + }, + "Server": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "server_addr": { + "type": "string" + }, + "speed_limit": { + "type": "integer", + "format": "int32" + }, + "traffic_ratio": { + "type": "number", + "format": "float" + }, + "groupId": { + "type": "integer", + "format": "int64" + }, + "protocol": { + "type": "string" + }, + "enable": { + "type": "boolean", + "format": "boolean" + }, + "vmess": { + "$ref": "#/definitions/Vmess" + }, + "vless": { + "$ref": "#/definitions/Vless" + }, + "trojan": { + "$ref": "#/definitions/Trojan" + }, + "shadowsocks": { + "$ref": "#/definitions/Shadowsocks" + }, + "enable_relay": { + "type": "boolean", + "format": "boolean" + }, + "relay_host": { + "type": "string" + }, + "relay_port": { + "type": "integer", + "format": "int32" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "updated_at": { + "type": "integer", + "format": "int64" + } + }, + "title": "Server", + "required": [ + "id", + "name", + "server_addr", + "speed_limit", + "traffic_ratio", + "groupId", + "protocol", + "enable", + "enable_relay", + "relay_host", + "relay_port", + "created_at", + "updated_at" + ] + }, + "ServerGroup": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "updated_at": { + "type": "integer", + "format": "int64" + } + }, + "title": "ServerGroup", + "required": [ + "id", + "name", + "description", + "created_at", + "updated_at" + ] + }, + "Shadowsocks": { + "type": "object", + "properties": { + "method": { + "type": "string" + }, + "port": { + "type": "integer", + "format": "int32" + } + }, + "title": "Shadowsocks", + "required": [ + "method", + "port" + ] + }, + "StripeConfig": { + "type": "object", + "properties": { + "public_key": { + "type": "string" + }, + "secret_key": { + "type": "string" + }, + "webhook_secret": { + "type": "string" + } + }, + "title": "StripeConfig", + "required": [ + "public_key", + "secret_key", + "webhook_secret" + ] + }, + "StripePaymentConfig": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "config": { + "$ref": "#/definitions/StripeConfig" + }, + "fee_mode": { + "type": "integer", + "format": "uint32" + }, + "fee_percent": { + "type": "integer", + "format": "int64" + }, + "fee_amount": { + "type": "integer", + "format": "int64" + }, + "enable": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "StripePaymentConfig", + "required": [ + "id", + "name", + "config", + "fee_mode", + "enable" + ] + }, + "Subscribe": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "unit_price": { + "type": "integer", + "format": "int64" + }, + "discount": { + "type": "array", + "items": { + "$ref": "#/definitions/SubscribeDiscount" + } + }, + "replacement": { + "type": "integer", + "format": "int64" + }, + "inventory": { + "type": "integer", + "format": "int64" + }, + "traffic": { + "type": "integer", + "format": "int64" + }, + "speed_limit": { + "type": "integer", + "format": "int64" + }, + "device_limit": { + "type": "integer", + "format": "int64" + }, + "quota": { + "type": "integer", + "format": "int64" + }, + "group_id": { + "type": "integer", + "format": "int64" + }, + "server_group": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "server": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "show": { + "type": "boolean", + "format": "boolean" + }, + "sell": { + "type": "boolean", + "format": "boolean" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "updated_at": { + "type": "integer", + "format": "int64" + } + }, + "title": "Subscribe", + "required": [ + "id", + "name", + "description", + "unit_price", + "discount", + "replacement", + "inventory", + "traffic", + "speed_limit", + "device_limit", + "quota", + "group_id", + "server_group", + "server", + "show", + "sell", + "created_at", + "updated_at" + ] + }, + "SubscribeDiscount": { + "type": "object", + "properties": { + "months": { + "type": "integer", + "format": "int64" + }, + "discount": { + "type": "integer", + "format": "int64" + } + }, + "title": "SubscribeDiscount", + "required": [ + "months", + "discount" + ] + }, + "SubscribeGroup": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "updated_at": { + "type": "integer", + "format": "int64" + } + }, + "title": "SubscribeGroup", + "required": [ + "id", + "name", + "description", + "created_at", + "updated_at" + ] + }, + "TestEmailSmtpRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + } + }, + "title": "TestEmailSmtpRequest", + "required": [ + "email" + ] + }, + "Ticket": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "user_id": { + "type": "integer", + "format": "int64" + }, + "follow": { + "type": "array", + "items": { + "$ref": "#/definitions/Follow" + } + }, + "status": { + "type": "integer", + "format": "uint8" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "updated_at": { + "type": "integer", + "format": "int64" + } + }, + "title": "Ticket", + "required": [ + "id", + "title", + "description", + "user_id", + "status", + "created_at", + "updated_at" + ] + }, + "Trojan": { + "type": "object", + "properties": { + "network": { + "type": "string" + }, + "host": { + "type": "string" + }, + "port": { + "type": "integer", + "format": "int32" + }, + "path": { + "type": "string" + } + }, + "title": "Trojan", + "required": [ + "network", + "host", + "port", + "path" + ] + }, + "UpdateAnnouncementEnableRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "enable": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "UpdateAnnouncementEnableRequest", + "required": [ + "id", + "enable" + ] + }, + "UpdateAnnouncementRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "title": { + "type": "string" + }, + "content": { + "type": "string" + }, + "enable": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "UpdateAnnouncementRequest", + "required": [ + "id", + "title", + "content", + "enable" + ] + }, + "UpdateApplicationRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "subscribe_type": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "title": "UpdateApplicationRequest", + "required": [ + "id", + "name", + "subscribe_type", + "icon", + "url" + ] + }, + "UpdateCouponRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "code": { + "type": "string" + }, + "count": { + "type": "integer", + "format": "int64" + }, + "type": { + "type": "integer", + "format": "uint8" + }, + "discount": { + "type": "integer", + "format": "int64" + }, + "start_time": { + "type": "integer", + "format": "int64" + }, + "expire_time": { + "type": "integer", + "format": "int64" + }, + "user_limit": { + "type": "integer", + "format": "int64" + }, + "subscribe": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "used_count": { + "type": "integer", + "format": "int64" + }, + "enable": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "UpdateCouponRequest", + "required": [ + "id", + "name", + "type", + "discount", + "start_time", + "expire_time" + ] + }, + "UpdateDocumentGroupRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "title": "UpdateDocumentGroupRequest", + "required": [ + "id", + "name", + "description" + ] + }, + "UpdateDocumentRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "title": { + "type": "string" + }, + "content": { + "type": "string" + }, + "group_id": { + "type": "integer", + "format": "int64" + }, + "show": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "UpdateDocumentRequest", + "required": [ + "id", + "title", + "content", + "group_id", + "show" + ] + }, + "UpdateEmailSmtpConfigRequest": { + "type": "object", + "properties": { + "email_smtp_host": { + "type": "string" + }, + "email_smtp_port": { + "type": "integer", + "format": "int64" + }, + "email_smtp_user": { + "type": "string" + }, + "email_smtp_pass": { + "type": "string" + }, + "email_smtp_from": { + "type": "string" + }, + "email_smtp_ssl": { + "type": "boolean", + "format": "boolean" + }, + "email_template": { + "type": "string" + } + }, + "title": "UpdateEmailSmtpConfigRequest", + "required": [ + "email_smtp_host", + "email_smtp_port", + "email_smtp_user", + "email_smtp_pass", + "email_smtp_from", + "email_smtp_ssl", + "email_template" + ] + }, + "UpdateInviteConfigRequest": { + "type": "object", + "properties": { + "forced_invite": { + "type": "boolean", + "format": "boolean" + }, + "referral_percentage": { + "type": "integer", + "format": "int64" + }, + "only_first_purchase": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "UpdateInviteConfigRequest", + "required": [ + "forced_invite", + "referral_percentage", + "only_first_purchase" + ] + }, + "UpdateNodeConfigRequest": { + "type": "object", + "properties": { + "node_secret": { + "type": "string" + }, + "node_pull_interval": { + "type": "integer", + "format": "int64" + }, + "node_push_interval": { + "type": "integer", + "format": "int64" + } + }, + "title": "UpdateNodeConfigRequest", + "required": [ + "node_secret", + "node_pull_interval", + "node_push_interval" + ] + }, + "UpdateNodeGroupRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "title": "UpdateNodeGroupRequest", + "required": [ + "id", + "name", + "description" + ] + }, + "UpdateNodeRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "server_addr": { + "type": "string" + }, + "speed_limit": { + "type": "integer", + "format": "int32" + }, + "traffic_ratio": { + "type": "number", + "format": "float" + }, + "groupId": { + "type": "integer", + "format": "int64" + }, + "protocol": { + "type": "string" + }, + "enable": { + "type": "boolean", + "format": "boolean" + }, + "vmess": { + "$ref": "#/definitions/Vmess" + }, + "vless": { + "$ref": "#/definitions/Vless" + }, + "trojan": { + "$ref": "#/definitions/Trojan" + }, + "shadowsocks": { + "$ref": "#/definitions/Shadowsocks" + }, + "enable_relay": { + "type": "boolean", + "format": "boolean" + }, + "relay_host": { + "type": "string" + }, + "relay_port": { + "type": "integer", + "format": "int32" + } + }, + "title": "UpdateNodeRequest", + "required": [ + "id", + "name", + "server_addr", + "speed_limit", + "traffic_ratio", + "groupId", + "protocol", + "enable", + "enable_relay", + "relay_host", + "relay_port" + ] + }, + "UpdateOrderStatusRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "status": { + "type": "integer", + "format": "uint8" + }, + "method": { + "type": "string" + }, + "trade_no": { + "type": "string" + } + }, + "title": "UpdateOrderStatusRequest", + "required": [ + "id", + "status" + ] + }, + "UpdateRegisterConfigRequest": { + "type": "object", + "properties": { + "stop_register": { + "type": "boolean", + "format": "boolean" + }, + "enable_trial": { + "type": "boolean", + "format": "boolean" + }, + "enable_email_verify": { + "type": "boolean", + "format": "boolean" + }, + "enable_email_domain_suffix": { + "type": "boolean", + "format": "boolean" + }, + "email_domain_suffix_list": { + "type": "string" + }, + "enable_ip_register_limit": { + "type": "boolean", + "format": "boolean" + }, + "ip_register_limit": { + "type": "integer", + "format": "int64" + }, + "ip_register_limit_duration": { + "type": "integer", + "format": "int64" + } + }, + "title": "UpdateRegisterConfigRequest", + "required": [ + "stop_register", + "enable_trial", + "enable_email_verify", + "enable_email_domain_suffix", + "email_domain_suffix_list", + "enable_ip_register_limit", + "ip_register_limit", + "ip_register_limit_duration" + ] + }, + "UpdateSiteConfigRequest": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "site_name": { + "type": "string" + }, + "site_desc": { + "type": "string" + }, + "site_logo": { + "type": "string" + } + }, + "title": "UpdateSiteConfigRequest", + "required": [ + "host", + "site_name", + "site_desc", + "site_logo" + ] + }, + "UpdateSubscribeConfigRequest": { + "type": "object", + "properties": { + "single_model": { + "type": "boolean", + "format": "boolean" + }, + "subscribe_path": { + "type": "string" + }, + "subscribe_domain": { + "type": "string" + }, + "pan_domain": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "UpdateSubscribeConfigRequest", + "required": [ + "single_model", + "subscribe_path", + "subscribe_domain", + "pan_domain" + ] + }, + "UpdateSubscribeGroupRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "title": "UpdateSubscribeGroupRequest", + "required": [ + "id", + "name", + "description" + ] + }, + "UpdateSubscribeRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "unit_price": { + "type": "integer", + "format": "int64" + }, + "discount": { + "type": "array", + "items": { + "$ref": "#/definitions/SubscribeDiscount" + } + }, + "replacement": { + "type": "integer", + "format": "int64" + }, + "inventory": { + "type": "integer", + "format": "int64" + }, + "traffic": { + "type": "integer", + "format": "int64" + }, + "speed_limit": { + "type": "integer", + "format": "int64" + }, + "device_limit": { + "type": "integer", + "format": "int64" + }, + "quota": { + "type": "integer", + "format": "int64" + }, + "group_id": { + "type": "integer", + "format": "int64" + }, + "server_group": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "server": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "show": { + "type": "boolean", + "format": "boolean" + }, + "sell": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "UpdateSubscribeRequest", + "required": [ + "id", + "name", + "description", + "unit_price", + "discount", + "replacement", + "inventory", + "traffic", + "speed_limit", + "device_limit", + "quota", + "group_id", + "server_group", + "server", + "show", + "sell" + ] + }, + "UpdateTelegramConfigRequest": { + "type": "object", + "properties": { + "telegram_bot_token": { + "type": "string" + }, + "telegram_group_url": { + "type": "string" + }, + "telegram_notify": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "UpdateTelegramConfigRequest", + "required": [ + "telegram_bot_token", + "telegram_group_url", + "telegram_notify" + ] + }, + "UpdateTosConfigRequest": { + "type": "object", + "properties": { + "tos_content": { + "type": "string" + } + }, + "title": "UpdateTosConfigRequest", + "required": [ + "tos_content" + ] + }, + "UpdateUserRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "avatar": { + "type": "string" + }, + "balance": { + "type": "integer", + "format": "int64" + }, + "telegram": { + "type": "integer", + "format": "int64" + }, + "refer_code": { + "type": "string" + }, + "referer_id": { + "type": "integer", + "format": "int64" + }, + "enable": { + "type": "boolean", + "format": "boolean" + }, + "is_admin": { + "type": "boolean", + "format": "boolean" + }, + "valid_email": { + "type": "boolean", + "format": "boolean" + }, + "enable_email_notify": { + "type": "boolean", + "format": "boolean" + }, + "enable_telegram_notify": { + "type": "boolean", + "format": "boolean" + }, + "enable_balance_notify": { + "type": "boolean", + "format": "boolean" + }, + "enable_login_notify": { + "type": "boolean", + "format": "boolean" + }, + "enable_subscribe_notify": { + "type": "boolean", + "format": "boolean" + }, + "enable_trade_notify": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "UpdateUserRequest", + "required": [ + "id", + "email", + "password", + "avatar", + "balance", + "telegram", + "refer_code", + "referer_id", + "enable", + "is_admin", + "valid_email", + "enable_email_notify", + "enable_telegram_notify", + "enable_balance_notify", + "enable_login_notify", + "enable_subscribe_notify", + "enable_trade_notify" + ] + }, + "UpdateVerifyConfigRequest": { + "type": "object", + "properties": { + "turnstile_site_key": { + "type": "string" + }, + "turnstile_secret": { + "type": "string" + }, + "enable_login_verify": { + "type": "boolean", + "format": "boolean" + }, + "enable_register_verify": { + "type": "boolean", + "format": "boolean" + }, + "enable_reset_password_verify": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "UpdateVerifyConfigRequest", + "required": [ + "turnstile_site_key", + "turnstile_secret", + "enable_login_verify", + "enable_register_verify", + "enable_reset_password_verify" + ] + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "email": { + "type": "string" + }, + "avatar": { + "type": "string" + }, + "balance": { + "type": "integer", + "format": "int64" + }, + "telegram": { + "type": "integer", + "format": "int64" + }, + "refer_code": { + "type": "string" + }, + "referer_id": { + "type": "integer", + "format": "int64" + }, + "enable": { + "type": "boolean", + "format": "boolean" + }, + "is_admin": { + "type": "boolean", + "format": "boolean" + }, + "valid_email": { + "type": "boolean", + "format": "boolean" + }, + "enable_email_notify": { + "type": "boolean", + "format": "boolean" + }, + "enable_telegram_notify": { + "type": "boolean", + "format": "boolean" + }, + "enable_balance_notify": { + "type": "boolean", + "format": "boolean" + }, + "enable_login_notify": { + "type": "boolean", + "format": "boolean" + }, + "enable_subscribe_notify": { + "type": "boolean", + "format": "boolean" + }, + "enable_trade_notify": { + "type": "boolean", + "format": "boolean" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "updated_at": { + "type": "integer", + "format": "int64" + }, + "deleted_at": { + "type": "integer", + "format": "int64" + }, + "is_del": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "User", + "required": [ + "id", + "email", + "avatar", + "balance", + "telegram", + "refer_code", + "referer_id", + "enable", + "is_admin", + "valid_email", + "enable_email_notify", + "enable_telegram_notify", + "enable_balance_notify", + "enable_login_notify", + "enable_subscribe_notify", + "enable_trade_notify", + "created_at", + "updated_at" + ] + }, + "UserLoginRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "cf_token": { + "type": "string" + } + }, + "title": "UserLoginRequest", + "required": [ + "email", + "password" + ] + }, + "UserRegisterRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "code": { + "type": "string" + }, + "cf_token": { + "type": "string" + } + }, + "title": "UserRegisterRequest", + "required": [ + "email", + "password" + ] + }, + "Vless": { + "type": "object", + "properties": { + "network": { + "type": "string" + }, + "host": { + "type": "string" + }, + "port": { + "type": "integer", + "format": "int32" + }, + "path": { + "type": "string" + }, + "tls": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "Vless", + "required": [ + "network", + "host", + "port", + "path", + "tls" + ] + }, + "Vmess": { + "type": "object", + "properties": { + "network": { + "type": "string" + }, + "host": { + "type": "string" + }, + "port": { + "type": "integer", + "format": "int32" + }, + "path": { + "type": "string" + }, + "tls": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "Vmess", + "required": [ + "network", + "host", + "port", + "path", + "tls" + ] + } + }, + "securityDefinitions": { + "apiKey": { + "type": "apiKey", + "description": "Enter JWT Bearer token **_only_**", + "name": "Authorization", + "in": "header" + } + } +} diff --git a/queue/handler/routes.go b/queue/handler/routes.go new file mode 100644 index 0000000..bb315d1 --- /dev/null +++ b/queue/handler/routes.go @@ -0,0 +1,39 @@ +package handler + +import ( + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/svc" + countrylogic "github.com/perfect-panel/ppanel-server/queue/logic/country" + orderLogic "github.com/perfect-panel/ppanel-server/queue/logic/order" + smslogic "github.com/perfect-panel/ppanel-server/queue/logic/sms" + "github.com/perfect-panel/ppanel-server/queue/logic/subscription" + "github.com/perfect-panel/ppanel-server/queue/logic/traffic" + "github.com/perfect-panel/ppanel-server/queue/types" + + emailLogic "github.com/perfect-panel/ppanel-server/queue/logic/email" +) + +func RegisterHandlers(mux *asynq.ServeMux, serverCtx *svc.ServiceContext) { + // get country task + mux.Handle(types.ForthwithGetCountry, countrylogic.NewGetNodeCountryLogic(serverCtx)) + // Send email task + mux.Handle(types.ForthwithSendEmail, emailLogic.NewSendEmailLogic(serverCtx)) + // Send sms task + mux.Handle(types.ForthwithSendSms, smslogic.NewSendSmsLogic(serverCtx)) + + // Defer close order task + mux.Handle(types.DeferCloseOrder, orderLogic.NewDeferCloseOrderLogic(serverCtx)) + // Forthwith activate order task + mux.Handle(types.ForthwithActivateOrder, orderLogic.NewActivateOrderLogic(serverCtx)) + + // Forthwith traffic statistics + mux.Handle(types.ForthwithTrafficStatistics, traffic.NewTrafficStatisticsLogic(serverCtx)) + + // Schedule check subscription + mux.Handle(types.SchedulerCheckSubscription, subscription.NewCheckSubscriptionLogic(serverCtx)) + + // Schedule total server data + mux.Handle(types.SchedulerTotalServerData, traffic.NewServerDataLogic(serverCtx)) + //定时查单 + mux.Handle(types.SchedulerCheckOrder, orderLogic.NewCheckOrderLogic(serverCtx)) +} diff --git a/queue/logic/country/getCountryLogic.go b/queue/logic/country/getCountryLogic.go new file mode 100644 index 0000000..d3e9236 --- /dev/null +++ b/queue/logic/country/getCountryLogic.go @@ -0,0 +1,60 @@ +package countrylogic + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/ip" + "github.com/perfect-panel/ppanel-server/queue/types" +) + +type GetNodeCountryLogic struct { + svcCtx *svc.ServiceContext +} + +func NewGetNodeCountryLogic(svcCtx *svc.ServiceContext) *GetNodeCountryLogic { + return &GetNodeCountryLogic{ + svcCtx: svcCtx, + } +} +func (l *GetNodeCountryLogic) ProcessTask(ctx context.Context, task *asynq.Task) error { + var payload types.GetNodeCountry + if err := json.Unmarshal(task.Payload(), &payload); err != nil { + logger.WithContext(ctx).Error("[GetNodeCountryLogic] Unmarshal payload failed", + logger.Field("error", err.Error()), + logger.Field("payload", task.Payload()), + ) + return nil + } + serverAddr := payload.ServerAddr + resp, err := ip.GetRegionByIp(serverAddr) + if err != nil { + logger.WithContext(ctx).Error("[GetNodeCountryLogic] ", logger.Field("error", err.Error()), logger.Field("serverAddr", serverAddr)) + return nil + } + + servers, err := l.svcCtx.ServerModel.FindNodeByServerAddrAndProtocol(ctx, payload.ServerAddr, payload.Protocol) + if err != nil { + logger.WithContext(ctx).Error("[GetNodeCountryLogic] FindNodeByServerAddrAnd", logger.Field("error", err.Error()), logger.Field("serverAddr", serverAddr)) + return err + } + if len(servers) == 0 { + return nil + } + for _, ser := range servers { + ser.Country = resp.Country + ser.City = resp.City + ser.Latitude = resp.Latitude + ser.Longitude = resp.Longitude + err := l.svcCtx.ServerModel.Update(ctx, ser) + if err != nil { + logger.WithContext(ctx).Error("[GetNodeCountryLogic] ", logger.Field("error", err.Error()), logger.Field("id", ser.Id)) + } + } + logger.WithContext(ctx).Info("[GetNodeCountryLogic] ", logger.Field("country", resp.Country), logger.Field("city", resp.Country)) + return nil +} diff --git a/queue/logic/email/sendEmailLogic.go b/queue/logic/email/sendEmailLogic.go new file mode 100644 index 0000000..393a796 --- /dev/null +++ b/queue/logic/email/sendEmailLogic.go @@ -0,0 +1,60 @@ +package emailLogic + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/model/log" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/email" + "github.com/perfect-panel/ppanel-server/queue/types" +) + +type SendEmailLogic struct { + svcCtx *svc.ServiceContext +} + +func NewSendEmailLogic(svcCtx *svc.ServiceContext) *SendEmailLogic { + return &SendEmailLogic{ + svcCtx: svcCtx, + } +} +func (l *SendEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) error { + var payload types.SendEmailPayload + if err := json.Unmarshal(task.Payload(), &payload); err != nil { + logger.WithContext(ctx).Error("[SendEmailLogic] Unmarshal payload failed", + logger.Field("error", err.Error()), + logger.Field("payload", task.Payload()), + ) + return nil + } + messageLog := log.MessageLog{ + Type: log.Email.String(), + Platform: l.svcCtx.Config.Email.Platform, + To: payload.Email, + Subject: payload.Subject, + Content: payload.Content, + } + sender, err := email.NewSender(l.svcCtx.Config.Email.Platform, l.svcCtx.Config.Email.PlatformConfig, l.svcCtx.Config.Site.SiteName) + if err != nil { + logger.WithContext(ctx).Error("[SendEmailLogic] NewSender failed", logger.Field("error", err.Error())) + return nil + } + err = sender.Send([]string{payload.Email}, payload.Subject, payload.Content) + if err != nil { + logger.WithContext(ctx).Error("[SendEmailLogic] Send email failed", logger.Field("error", err.Error())) + return nil + } + messageLog.Status = 1 + if err = l.svcCtx.LogModel.InsertMessageLog(ctx, &messageLog); err != nil { + logger.WithContext(ctx).Error("[SendEmailLogic] InsertMessageLog failed", + logger.Field("error", err.Error()), + logger.Field("messageLog", messageLog), + ) + } + logger.WithContext(ctx).Info("[SendEmailLogic] Send email", logger.Field("email", payload.Email), logger.Field("content", payload.Content)) + return nil +} diff --git a/queue/logic/order/activateOrderLogic.go b/queue/logic/order/activateOrderLogic.go new file mode 100644 index 0000000..19b164d --- /dev/null +++ b/queue/logic/order/activateOrderLogic.go @@ -0,0 +1,691 @@ +package orderLogic + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/constant" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/google/uuid" + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/logic/telegram" + "github.com/perfect-panel/ppanel-server/internal/model/order" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/ppanel-server/pkg/uuidx" + "github.com/perfect-panel/ppanel-server/queue/types" + "gorm.io/gorm" +) + +const ( + Subscribe = 1 + Renewal = 2 + ResetTraffic = 3 + Recharge = 4 +) + +type ActivateOrderLogic struct { + svc *svc.ServiceContext +} + +func NewActivateOrderLogic(svc *svc.ServiceContext) *ActivateOrderLogic { + return &ActivateOrderLogic{ + svc: svc, + } +} + +func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task) error { + payload := types.ForthwithActivateOrderPayload{} + if err := json.Unmarshal(task.Payload(), &payload); err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Unmarshal payload failed", + logger.Field("error", err.Error()), + logger.Field("payload", string(task.Payload())), + ) + return nil + } + // Find order by order no + orderInfo, err := l.svc.OrderModel.FindOneByOrderNo(ctx, payload.OrderNo) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find order failed", + logger.Field("error", err.Error()), + logger.Field("order_no", payload.OrderNo), + ) + return nil + } + + if orderInfo.Status != 2 { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Order status error", + logger.Field("order_no", orderInfo.OrderNo), + logger.Field("status", orderInfo.Status), + ) + return nil + } + switch orderInfo.Type { + case Subscribe: + err = l.NewPurchase(ctx, orderInfo) + case Renewal: + err = l.Renewal(ctx, orderInfo) + case ResetTraffic: + err = l.ResetTraffic(ctx, orderInfo) + case Recharge: + err = l.Recharge(ctx, orderInfo) + default: + logger.WithContext(ctx).Error("[ActivateOrderLogic] Order type is invalid", logger.Field("type", orderInfo.Type)) + } + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Process task failed", logger.Field("error", err.Error())) + return nil + } + // if coupon is not empty + if orderInfo.Coupon != "" { + // update coupon status + err = l.svc.CouponModel.UpdateCount(ctx, orderInfo.Coupon) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Update coupon status failed", + logger.Field("error", err.Error()), + logger.Field("coupon", orderInfo.Coupon), + ) + } + } + // update order status + orderInfo.Status = 5 + err = l.svc.OrderModel.Update(ctx, orderInfo) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Update order status failed", + logger.Field("error", err.Error()), + logger.Field("order_no", orderInfo.OrderNo), + ) + } + + return nil +} + +// NewPurchase New purchase +func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.Order) error { + var userInfo *user.User + var err error + if orderInfo.UserId != 0 { + // find user by user id + userInfo, err = l.svc.UserModel.FindOne(ctx, orderInfo.UserId) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user failed", + logger.Field("error", err.Error()), + logger.Field("user_id", orderInfo.UserId), + logger.Field("user_id", orderInfo.UserId), + ) + return err + } + } else { + // If User ID is 0, it means that the order is a guest order, need to create a new user + // query info with redis + cacheKey := fmt.Sprintf(constant.TempOrderCacheKey, orderInfo.OrderNo) + data, err := l.svc.Redis.Get(ctx, cacheKey).Result() + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Get temp order cache failed", + logger.Field("error", err.Error()), + logger.Field("cache_key", cacheKey), + ) + return err + } + var tempOrder constant.TemporaryOrderInfo + if err = json.Unmarshal([]byte(data), &tempOrder); err != nil { + logger.WithContext(ctx).Errorw("[ActivateOrderLogic] Unmarshal temp order failed", + logger.Field("error", err.Error()), + ) + return err + } + // create user + + userInfo = &user.User{ + Password: tool.EncodePassWord(tempOrder.Password), + AuthMethods: []user.AuthMethods{ + { + AuthType: tempOrder.AuthType, + AuthIdentifier: tempOrder.Identifier, + }, + }, + } + err = l.svc.UserModel.Transaction(ctx, func(tx *gorm.DB) error { + // Save user information + if err := tx.Save(userInfo).Error; err != nil { + return err + } + // Generate ReferCode + userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id) + // Update ReferCode + if err := tx.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil { + return err + } + orderInfo.UserId = userInfo.Id + return tx.Model(&order.Order{}).Where("order_no = ?", orderInfo.OrderNo).Update("user_id", userInfo.Id).Error + }) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Create user failed", + logger.Field("error", err.Error()), + ) + return err + } + logger.WithContext(ctx).Info("[ActivateOrderLogic] Create guest user success", logger.Field("user_id", userInfo.Id), logger.Field("Identifier", tempOrder.Identifier), logger.Field("AuthType", tempOrder.AuthType)) + } + // find subscribe by id + sub, err := l.svc.SubscribeModel.FindOne(ctx, orderInfo.SubscribeId) + if err != nil { + logger.WithContext(ctx).Errorw("[ActivateOrderLogic] Find subscribe failed", + logger.Field("error", err.Error()), + logger.Field("subscribe_id", orderInfo.SubscribeId), + ) + return err + } + // create user subscribe + now := time.Now() + + //系统开启了试用订阅功能,并且当前存在试用订阅 + if l.svc.Config.Register.EnableTrial && l.svc.Config.Register.TrialSubscribe != 0 { + //查询使用订阅套餐 + subscribeDetails, subErr := l.svc.UserModel.QueryUserSubscribe(ctx, userInfo.Id, 1, 2) + if subErr != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] disable user try out subscribe failed", + logger.Field("error", subErr.Error()), + ) + } else { + for _, item := range subscribeDetails { + if item.Subscribe.Id == l.svc.Config.Register.TrialSubscribe { + err = l.svc.UserModel.UpdateSubscribe(ctx, &user.Subscribe{ + Id: item.Id, + UserId: item.UserId, + OrderId: item.OrderId, + SubscribeId: item.SubscribeId, + StartTime: item.StartTime, + ExpireTime: now, + Traffic: item.Traffic, + Download: item.Download, + Upload: item.Upload, + Token: item.Token, + UUID: item.UUID, + FinishedAt: now, + Status: 3, + }) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] disable user try out subscribe failed", + logger.Field("error", err.Error()), + ) + } else { + logger.WithContext(ctx).Info("[ActivateOrderLogic] disable user try out subscribe success", + logger.Field("user_id", userInfo.Id), + logger.Field("subscribe_id", item.SubscribeId), + ) + } + break + } + } + } + } + + userSub := user.Subscribe{ + Id: 0, + UserId: orderInfo.UserId, + OrderId: orderInfo.Id, + SubscribeId: orderInfo.SubscribeId, + StartTime: now, + ExpireTime: tool.AddTime(sub.UnitTime, orderInfo.Quantity, now), + Traffic: sub.Traffic, + Download: 0, + Upload: 0, + Token: uuidx.SubscribeToken(orderInfo.OrderNo), + UUID: uuid.New().String(), + Status: 1, + } + err = l.svc.UserModel.InsertSubscribe(ctx, &userSub) + + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Insert user subscribe failed", + logger.Field("error", err.Error()), + ) + return err + } + // handler commission + if userInfo.RefererId != 0 && + l.svc.Config.Invite.ReferralPercentage != 0 && + (!l.svc.Config.Invite.OnlyFirstPurchase || orderInfo.IsNew) { + referer, err := l.svc.UserModel.FindOne(ctx, userInfo.RefererId) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find referer failed", + logger.Field("error", err.Error()), + logger.Field("referer_id", userInfo.RefererId), + ) + goto updateCache + } + // calculate commission + amount := float64(orderInfo.Price) * (float64(l.svc.Config.Invite.ReferralPercentage) / 100) + referer.Commission += int64(amount) + err = l.svc.UserModel.Update(ctx, referer) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Update referer commission failed", + logger.Field("error", err.Error()), + ) + goto updateCache + } + // create commission log + commissionLog := user.CommissionLog{ + UserId: referer.Id, + OrderNo: orderInfo.OrderNo, + Amount: int64(amount), + } + err = l.svc.UserModel.InsertCommissionLog(ctx, &commissionLog) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Insert commission log failed", + logger.Field("error", err.Error()), + ) + } + err = l.svc.UserModel.UpdateUserCache(ctx, referer) + if err != nil { + logger.WithContext(ctx).Errorw("[ActivateOrderLogic] Update referer cache", logger.Field("error", err.Error()), logger.Field("user_id", referer.Id)) + } + } +updateCache: + for _, id := range tool.StringToInt64Slice(sub.Server) { + cacheKey := fmt.Sprintf("%s%d", config.ServerUserListCacheKey, id) + err = l.svc.Redis.Del(ctx, cacheKey).Err() + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Del server user list cache failed", + logger.Field("error", err.Error()), + logger.Field("cache_key", cacheKey), + ) + } + } + data, err := l.svc.ServerModel.FindServerListByGroupIds(ctx, tool.StringToInt64Slice(sub.ServerGroup)) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find server list failed", logger.Field("error", err.Error())) + return nil + } + for _, item := range data { + cacheKey := fmt.Sprintf("%s%d", config.ServerUserListCacheKey, item.Id) + err = l.svc.Redis.Del(ctx, cacheKey).Err() + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Del server user list cache failed", + logger.Field("error", err.Error()), + logger.Field("cache_key", cacheKey), + ) + } + } + userTelegramChatId, ok := findTelegram(userInfo) + + // sendMessage To Telegram + if ok { + text, err := tool.RenderTemplateToString(telegram.PurchaseNotify, map[string]string{ + "OrderNo": orderInfo.OrderNo, + "SubscribeName": sub.Name, + "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), + "ExpireTime": userSub.ExpireTime.Format("2006-01-02 15:04:05"), + }) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Render template failed", + logger.Field("error", err.Error()), + ) + } + l.sendUserNotifyWithTelegram(userTelegramChatId, text) + } + // send message to admin + text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, map[string]string{ + "OrderNo": orderInfo.OrderNo, + "TradeNo": orderInfo.TradeNo, + "SubscribeName": sub.Name, + //"UserEmail": userInfo.Email, + "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), + "OrderStatus": "已支付", + "OrderTime": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"), + "PaymentMethod": orderInfo.Method, + }) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Render AdminOrderNotify template failed", + logger.Field("error", err.Error()), + ) + } + l.sendAdminNotifyWithTelegram(ctx, text) + logger.WithContext(ctx).Info("[ActivateOrderLogic] Insert user subscribe success") + return nil +} + +// Renewal Renewal +func (l *ActivateOrderLogic) Renewal(ctx context.Context, orderInfo *order.Order) error { + // find user by user id + userInfo, err := l.svc.UserModel.FindOne(ctx, orderInfo.UserId) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user failed", + logger.Field("error", err.Error()), + logger.Field("user_id", orderInfo.UserId), + ) + return err + } + // find user subscribe by subscribe token + userSub, err := l.svc.UserModel.FindOneSubscribeByOrderId(ctx, orderInfo.ParentId) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user subscribe failed", + logger.Field("error", err.Error()), + logger.Field("order_id", orderInfo.Id), + ) + return err + } + // find subscribe by id + sub, err := l.svc.SubscribeModel.FindOne(ctx, orderInfo.SubscribeId) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find subscribe failed", + logger.Field("error", err.Error()), + logger.Field("subscribe_id", orderInfo.SubscribeId), + logger.Field("order_id", orderInfo.Id), + ) + return err + } + now := time.Now() + if userSub.ExpireTime.Before(now) { + userSub.ExpireTime = now + userSub.Status = 1 + } + + //fix bug:FinishedAt causes the update subscription to fail + if now.AddDate(-30, 0, 0).After(userSub.FinishedAt) { + userSub.FinishedAt = now + } + + userSub.ExpireTime = tool.AddTime(sub.UnitTime, orderInfo.Quantity, userSub.ExpireTime) + // update user subscribe + err = l.svc.UserModel.UpdateSubscribe(ctx, userSub) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Update user subscribe failed", + logger.Field("error", err.Error()), + ) + return err + } + // handler commission + if userInfo.RefererId != 0 && + l.svc.Config.Invite.ReferralPercentage != 0 && + !l.svc.Config.Invite.OnlyFirstPurchase { + referer, err := l.svc.UserModel.FindOne(ctx, userInfo.RefererId) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find referer failed", + logger.Field("error", err.Error()), + logger.Field("referer_id", userInfo.RefererId), + ) + goto sendMessage + } + // calculate commission + amount := float64(orderInfo.Price) * (float64(l.svc.Config.Invite.ReferralPercentage) / 100) + referer.Commission += int64(amount) + err = l.svc.UserModel.Update(ctx, referer) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Update referer commission failed", + logger.Field("error", err.Error()), + ) + goto sendMessage + } + // create commission log + commissionLog := user.CommissionLog{ + UserId: referer.Id, + OrderNo: orderInfo.OrderNo, + Amount: int64(amount), + } + err = l.svc.UserModel.InsertCommissionLog(ctx, &commissionLog) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Insert commission log failed", + logger.Field("error", err.Error()), + ) + } + err = l.svc.UserModel.UpdateUserCache(ctx, referer) + if err != nil { + logger.WithContext(ctx).Errorw("[ActivateOrderLogic] Update referer cache", logger.Field("error", err.Error()), logger.Field("user_id", referer.Id)) + } + } +sendMessage: + userTelegramChatId, ok := findTelegram(userInfo) + // SendMessage To Telegram + if ok { + text, err := tool.RenderTemplateToString(telegram.RenewalNotify, map[string]string{ + "OrderNo": orderInfo.OrderNo, + "SubscribeName": sub.Name, + "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), + "ExpireTime": userSub.ExpireTime.Format("2006-01-02 15:04:05"), + }) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Render template failed", + logger.Field("error", err.Error()), + ) + } + l.sendUserNotifyWithTelegram(userTelegramChatId, text) + } + + // send message to admin + text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, map[string]string{ + "OrderNo": orderInfo.OrderNo, + "TradeNo": orderInfo.TradeNo, + "SubscribeName": sub.Name, + //"UserEmail": userInfo.Email, + "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), + "OrderStatus": "已支付", + "OrderTime": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"), + "PaymentMethod": orderInfo.Method, + }) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Render AdminOrderNotify template failed", + logger.Field("error", err.Error()), + ) + } + l.sendAdminNotifyWithTelegram(ctx, text) + return nil +} + +// ResetTraffic Reset traffic +func (l *ActivateOrderLogic) ResetTraffic(ctx context.Context, orderInfo *order.Order) error { + // find user by user id + userInfo, err := l.svc.UserModel.FindOne(ctx, orderInfo.UserId) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user failed", + logger.Field("error", err.Error()), + logger.Field("user_id", orderInfo.UserId), + ) + return err + } + // Generate a Subscribe Token through orderNo + // find user subscribe by subscribe token + userSub, err := l.svc.UserModel.FindOneSubscribeByToken(ctx, orderInfo.SubscribeToken) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user subscribe failed", + logger.Field("error", err.Error()), + logger.Field("order_id", orderInfo.Id), + ) + return err + } + userSub.Download = 0 + userSub.Upload = 0 + userSub.Status = 1 + // update user subscribe + err = l.svc.UserModel.UpdateSubscribe(ctx, userSub) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Update user subscribe failed", + logger.Field("error", err.Error()), + ) + return err + } + sub, err := l.svc.SubscribeModel.FindOne(ctx, userSub.SubscribeId) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find subscribe failed", + logger.Field("error", err.Error()), + logger.Field("subscribe_id", userSub.SubscribeId), + ) + return nil + } + userTelegramChatId, ok := findTelegram(userInfo) + // SendMessage To Telegram + if ok { + text, err := tool.RenderTemplateToString(telegram.ResetTrafficNotify, map[string]string{ + "OrderNo": orderInfo.OrderNo, + "SubscribeName": sub.Name, + "ResetTime": time.Now().Format("2006-01-02 15:04:05"), + "ExpireTime": userSub.ExpireTime.Format("2006-01-02 15:04:05"), + }) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Render template failed", + logger.Field("error", err.Error()), + ) + } + l.sendUserNotifyWithTelegram(userTelegramChatId, text) + } + + // send message to admin + text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, map[string]string{ + "OrderNo": orderInfo.OrderNo, + "TradeNo": orderInfo.TradeNo, + "SubscribeName": "流量重置", + "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), + "OrderStatus": "已支付", + "OrderTime": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"), + "PaymentMethod": orderInfo.Method, + }) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Render AdminOrderNotify template failed", + logger.Field("error", err.Error()), + ) + } + l.sendAdminNotifyWithTelegram(ctx, text) + return nil +} + +// Recharge Recharge to user +func (l *ActivateOrderLogic) Recharge(ctx context.Context, orderInfo *order.Order) error { + // find user by user id + userInfo, err := l.svc.UserModel.FindOne(ctx, orderInfo.UserId) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user failed", + logger.Field("error", err.Error()), + logger.Field("user_id", orderInfo.UserId), + ) + return err + } + userInfo.Balance += orderInfo.Price + // update user + err = l.svc.DB.Transaction(func(tx *gorm.DB) error { + err = l.svc.UserModel.Update(ctx, userInfo, tx) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Update user failed", + logger.Field("error", err.Error()), + ) + return err + } + // Create Balance Log + balanceLog := user.BalanceLog{ + UserId: orderInfo.UserId, + Amount: orderInfo.Price, + Type: 1, + OrderId: orderInfo.Id, + Balance: userInfo.Balance, + } + err = l.svc.UserModel.InsertBalanceLog(ctx, &balanceLog, tx) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Insert balance log failed", + logger.Field("error", err.Error()), + ) + return err + } + + return nil + }) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Database transaction failed", + logger.Field("error", err.Error()), + ) + return err + } + userTelegramChatId, ok := findTelegram(userInfo) + // SendMessage To Telegram + if ok { + text, err := tool.RenderTemplateToString(telegram.RechargeNotify, map[string]string{ + "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), + "PaymentMethod": orderInfo.Method, + "Time": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"), + "Balance": fmt.Sprintf("%.2f", float64(userInfo.Balance)/100), + }) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Render template failed", + logger.Field("error", err.Error()), + ) + } + l.sendUserNotifyWithTelegram(userTelegramChatId, text) + } + // send message to admin + text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, map[string]string{ + "OrderNo": orderInfo.OrderNo, + "TradeNo": orderInfo.TradeNo, + "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), + "SubscribeName": "余额充值", + "OrderStatus": "已支付", + "OrderTime": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"), + "PaymentMethod": orderInfo.Method, + }) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Render AdminOrderNotify template failed", + logger.Field("error", err.Error()), + ) + } + l.sendAdminNotifyWithTelegram(ctx, text) + return nil +} + +// sendUserNotifyWithTelegram send message to user +func (l *ActivateOrderLogic) sendUserNotifyWithTelegram(chatId int64, text string) { + msg := tgbotapi.NewMessage(chatId, text) + msg.ParseMode = "markdown" + _, err := l.svc.TelegramBot.Send(msg) + if err != nil { + logger.Error("[ActivateOrderLogic] Send telegram user message failed", + logger.Field("error", err.Error()), + ) + } +} + +// sendAdminNotifyWithTelegram send message to admin +func (l *ActivateOrderLogic) sendAdminNotifyWithTelegram(ctx context.Context, text string) { + admins, err := l.svc.UserModel.QueryAdminUsers(ctx) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Query admin users failed", + logger.Field("error", err.Error()), + ) + return + } + for _, admin := range admins { + telegramId, ok := findTelegram(admin) + if !ok { + continue + } + msg := tgbotapi.NewMessage(telegramId, text) + msg.ParseMode = "markdown" + _, err := l.svc.TelegramBot.Send(msg) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Send telegram admin message failed", + logger.Field("error", err.Error()), + ) + } + } +} + +// findTelegram find user telegram id +func findTelegram(u *user.User) (int64, bool) { + for _, item := range u.AuthMethods { + if item.AuthType == "telegram" { + // string to int64 + parseInt, err := strconv.ParseInt(item.AuthIdentifier, 10, 64) + if err != nil { + return 0, false + } + return parseInt, true + } + + } + return 0, false +} diff --git a/queue/logic/order/checkOrderLogic.go b/queue/logic/order/checkOrderLogic.go new file mode 100644 index 0000000..6af678b --- /dev/null +++ b/queue/logic/order/checkOrderLogic.go @@ -0,0 +1,161 @@ +package orderLogic + +import ( + "context" + "encoding/json" + "github.com/hibiken/asynq" + order2 "github.com/perfect-panel/ppanel-server/internal/logic/public/order" + "github.com/perfect-panel/ppanel-server/internal/model/payment" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/pkg/payment/alipay" + "github.com/perfect-panel/ppanel-server/pkg/payment/payssion" + "github.com/perfect-panel/ppanel-server/pkg/payment/stripe" + "github.com/perfect-panel/ppanel-server/queue/types" + "go.uber.org/zap" +) + +type CheckOrderLogic struct { + svc *svc.ServiceContext +} + +func NewCheckOrderLogic(svc *svc.ServiceContext) *CheckOrderLogic { + return &CheckOrderLogic{ + svc: svc, + } +} + +func (l *CheckOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task) error { + + orderList, err := l.svc.OrderModel.QueryPendingOrders(ctx) + if err != nil { + logger.Errorf("query pending orders error: %v", zap.Error(err)) + return err + } + logger.Infof("查到订单数据: %v", orderList) + for _, order := range orderList { + paymentConfig, err := l.svc.PaymentModel.FindOne(ctx, order.PaymentId) + if err != nil { + logger.Errorw("[CheckOrder] Find payment config failed", logger.Field("error", err.Error()), logger.Field("paymentMark", order.Method)) + continue + } + logger.Infof("查到配置数据[%s]: %v", order.Method, orderList) + var flag bool + switch order.Method { + case order2.AlipayF2f: + if l.queryAlipay(paymentConfig, order.TradeNo) { + flag = true + } + break + case order2.Payssion: + logger.Infof("匹配配置类型: %v", order2.Payssion) + if l.queryPayssion(paymentConfig, order.OrderNo) { + flag = true + } + break + case order2.StripeWeChatPay: + if l.queryStripe(paymentConfig, order.TradeNo) { + flag = true + } + break + default: + logger.Infow("[CheckOrder] Unsupported payment method", logger.Field("paymentMethod", order.Method)) + continue + } + logger.Infof("[CheckOrder] Unsupported payment method[%v]", flag) + if flag { + err := l.svc.OrderModel.UpdateOrderStatus(ctx, order.OrderNo, 2) + if err != nil { + logger.Errorf("[CheckOrder] query order status error: %v", zap.Error(err)) + } + logger.Info("[CheckOrder] Notify status success", logger.Field("orderNo", order.TradeNo)) + payload := types.ForthwithActivateOrderPayload{ + OrderNo: order.OrderNo, + } + bytes, err := json.Marshal(&payload) + if err != nil { + logger.Error("[CheckOrder] Marshal payload failed", logger.Field("error", err.Error())) + return err + } + task := asynq.NewTask(types.ForthwithActivateOrder, bytes) + taskInfo, err := l.svc.Queue.EnqueueContext(ctx, task) + if err != nil { + logger.Error("[CheckOrder] Enqueue task failed", logger.Field("error", err.Error())) + return err + } + logger.Info("[CheckOrder] Enqueue task success", logger.Field("taskInfo", taskInfo)) + } + } + + return nil +} + +// queryAlipay Query Alipay payment status +// +//nolint:unused +func (l *CheckOrderLogic) queryAlipay(paymentConfig *payment.Payment, TradeNo string) bool { + config := payment.AlipayF2FConfig{} + if err := json.Unmarshal([]byte(paymentConfig.Config), &config); err != nil { + zap.S().Errorw("[CheckOrder] Unmarshal payment config failed", logger.Field("error", err.Error()), logger.Field("config", paymentConfig.Config)) + return false + } + client := alipay.NewClient(alipay.Config{ + AppId: config.AppId, + PrivateKey: config.PrivateKey, + PublicKey: config.PublicKey, + InvoiceName: config.InvoiceName, + }) + status, err := client.QueryTrade(context.Background(), TradeNo) + if err != nil { + zap.S().Errorw("[CheckOrder] Query trade failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo)) + return false + } + if status == alipay.Success || status == alipay.Finished { + return true + } + return false +} + +// queryStripe Query Stripe payment status +// +//nolint:unused +func (l *CheckOrderLogic) queryStripe(paymentConfig *payment.Payment, TradeNo string) bool { + config := payment.StripeConfig{} + if err := json.Unmarshal([]byte(paymentConfig.Config), &config); err != nil { + zap.S().Errorw("[CheckOrder] Unmarshal payment config failed", logger.Field("error", err.Error()), logger.Field("config", paymentConfig.Config)) + return false + } + client := stripe.NewClient(stripe.Config{ + PublicKey: config.PublicKey, + SecretKey: config.SecretKey, + WebhookSecret: config.WebhookSecret, + }) + status, err := client.QueryOrderStatus(TradeNo) + if err != nil { + zap.S().Errorw("[CheckOrder] Query order status failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo)) + return false + } + return status +} + +// queryPayssion Query Stripe payment status +// +//nolint:unused +func (l *CheckOrderLogic) queryPayssion(paymentConfig *payment.Payment, TradeNo string) bool { + zap.S().Infof("[CheckOrder]1 Query Payssion called") + payssionConfig := payment.PayssionConfig{} + if err := json.Unmarshal([]byte(paymentConfig.Config), &payssionConfig); err != nil { + zap.S().Errorw("[CheckOrder] Unmarshal error", logger.Field("error", err.Error())) + return false + } + zap.S().Infof("[CheckOrder]2 Query Payssion called") + client := payssion.NewClient(payssionConfig.ApiKey, payssionConfig.SecretKey, payssionConfig.PmId, payssionConfig.Currency, payssionConfig.QueryUrl, payssionConfig.CreateUrl) + // create payment + result, err := client.QueryOrder(TradeNo) + if err != nil { + zap.S().Errorw("[CheckOrder] Query order status failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo)) + return false + } + zap.S().Infof("[CheckOrder]3 Query Payssion called") + return result.Transaction.State == "completed" +} diff --git a/queue/logic/order/deferCloseOrderLogic.go b/queue/logic/order/deferCloseOrderLogic.go new file mode 100644 index 0000000..16a446e --- /dev/null +++ b/queue/logic/order/deferCloseOrderLogic.go @@ -0,0 +1,47 @@ +package orderLogic + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/logic/public/order" + "github.com/perfect-panel/ppanel-server/internal/svc" + internal "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/ppanel-server/queue/types" +) + +type DeferCloseOrderLogic struct { + svc *svc.ServiceContext +} + +func NewDeferCloseOrderLogic(svc *svc.ServiceContext) *DeferCloseOrderLogic { + return &DeferCloseOrderLogic{ + svc: svc, + } +} + +func (l *DeferCloseOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task) error { + payload := types.DeferCloseOrderPayload{} + if err := json.Unmarshal(task.Payload(), &payload); err != nil { + logger.WithContext(ctx).Error("[DeferCloseOrderLogic] Unmarshal payload failed", + logger.Field("error", err.Error()), + logger.Field("payload", string(task.Payload())), + ) + return nil + } + + err := order.NewCloseOrderLogic(ctx, l.svc).CloseOrder(&internal.CloseOrderRequest{ + OrderNo: payload.OrderNo, + }) + count, ok := asynq.GetRetryCount(ctx) + if !ok { + return nil + } + if err != nil && count < 3 { + return err + } + return nil +} diff --git a/queue/logic/sms/sendSmsLogic.go b/queue/logic/sms/sendSmsLogic.go new file mode 100644 index 0000000..ad61b9d --- /dev/null +++ b/queue/logic/sms/sendSmsLogic.go @@ -0,0 +1,73 @@ +package smslogic + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/model/log" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/ppanel-server/pkg/sms" + "github.com/perfect-panel/ppanel-server/queue/types" +) + +type SmsSendCount struct { + Count int `json:"count"` + CreateAt int64 `json:"create_at"` +} + +type SendSmsLogic struct { + svcCtx *svc.ServiceContext +} + +func NewSendSmsLogic(svcCtx *svc.ServiceContext) *SendSmsLogic { + return &SendSmsLogic{ + svcCtx: svcCtx, + } +} +func (l *SendSmsLogic) ProcessTask(ctx context.Context, task *asynq.Task) error { + var payload types.SendSmsPayload + if err := json.Unmarshal(task.Payload(), &payload); err != nil { + logger.WithContext(ctx).Error("[SendSmsLogic] Unmarshal payload failed", + logger.Field("error", err.Error()), + logger.Field("payload", task.Payload()), + ) + return nil + } + client, err := sms.NewSender(l.svcCtx.Config.Mobile.Platform, l.svcCtx.Config.Mobile.PlatformConfig) + if err != nil { + logger.WithContext(ctx).Error("[SendSmsLogic] New send sms client failed", logger.Field("error", err.Error()), logger.Field("payload", payload)) + return err + } + createSms := &log.MessageLog{ + Type: log.Mobile.String(), + Platform: l.svcCtx.Config.Mobile.Platform, + To: fmt.Sprintf("+%s%s", payload.TelephoneArea, payload.Telephone), + Subject: constant.ParseVerifyType(payload.Type).String(), + Content: "", + } + err = client.SendCode(payload.TelephoneArea, payload.Telephone, payload.Content) + + createSms.Content = client.GetSendCodeContent(payload.Content) + + if err != nil { + logger.WithContext(ctx).Error("[SendSmsLogic] Send sms failed", logger.Field("error", err.Error()), logger.Field("payload", payload)) + if l.svcCtx.Config.Model != constant.DevMode { + createSms.Status = 2 + } else { + return nil + } + } + createSms.Status = 1 + logger.WithContext(ctx).Info("[SendSmsLogic] Send sms", logger.Field("telephone", payload.Telephone), logger.Field("content", createSms.Content)) + err = l.svcCtx.LogModel.InsertMessageLog(ctx, createSms) + if err != nil { + logger.WithContext(ctx).Error("[SendSmsLogic] Send sms failed", logger.Field("error", err.Error()), logger.Field("payload", payload)) + return nil + } + return nil +} diff --git a/queue/logic/subscription/checkSubscriptionLogic.go b/queue/logic/subscription/checkSubscriptionLogic.go new file mode 100644 index 0000000..8cfe566 --- /dev/null +++ b/queue/logic/subscription/checkSubscriptionLogic.go @@ -0,0 +1,215 @@ +package subscription + +import ( + "bytes" + "context" + "encoding/json" + "text/template" + "time" + + queue "github.com/perfect-panel/ppanel-server/queue/types" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/ppanel-server/internal/svc" + "gorm.io/gorm" +) + +type CheckSubscriptionLogic struct { + svc *svc.ServiceContext +} + +func NewCheckSubscriptionLogic(svc *svc.ServiceContext) *CheckSubscriptionLogic { + return &CheckSubscriptionLogic{ + svc: svc, + } +} + +func (l *CheckSubscriptionLogic) ProcessTask(ctx context.Context, _ *asynq.Task) error { + logger.Infof("[CheckSubscription] Start check subscription: %s", time.Now().Format("2006-01-02 15:04:05")) + // Check subscription traffic + err := l.svc.UserModel.Transaction(ctx, func(db *gorm.DB) error { + var list []*user.Subscribe + err := db.Model(&user.Subscribe{}).Where("upload + download >= traffic AND status = 1 AND traffic > 0 ").Find(&list).Error + if err != nil { + logger.Errorw("[Check Subscription Traffic] Query subscribe failed", logger.Field("error", err.Error())) + return err + } + var ids []int64 + for _, item := range list { + ids = append(ids, item.Id) + } + if len(ids) > 0 { + err = db.Model(&user.Subscribe{}).Where("id IN ?", ids).Updates(map[string]interface{}{ + "status": 2, + "finished_at": time.Now(), + }).Error + if err != nil { + logger.Errorw("[Check Subscription Traffic] Update subscribe status failed", logger.Field("error", err.Error())) + return nil + } + err = l.sendTrafficNotify(ctx, ids) + if err != nil { + logger.Errorw("[Check Subscription Traffic] Send email failed", logger.Field("error", err.Error())) + return nil + } + + if len(list) > 0 { + if err = l.svc.UserModel.ClearSubscribeCache(ctx, list...); err != nil { + logger.Errorw("[Check Subscription Traffic] Clear subscribe cache failed", logger.Field("error", err.Error())) + return err + } + } + + logger.Infow("[Check Subscription Traffic] Update subscribe status", logger.Field("user_ids", ids), logger.Field("count", int64(len(ids)))) + + } else { + logger.Info("[Check Subscription Traffic] No subscribe need to update") + } + + return nil + }) + if err != nil { + logger.Error("[CheckSubscription] Transaction failed", logger.Field("error", err.Error())) + } + // Check subscription expire + err = l.svc.UserModel.Transaction(ctx, func(db *gorm.DB) error { + var list []*user.Subscribe + err = db.Model(&user.Subscribe{}).Where("`status` = 1 AND `expire_time` < ? AND `expire_time` != ? and `finished_at` IS NULL", time.Now(), time.UnixMilli(0)).Find(&list).Error + if err != nil { + logger.Error("[Check Subscription] Find subscribe failed", logger.Field("error", err.Error())) + return err + } + var ids []int64 + for _, item := range list { + ids = append(ids, item.Id) + } + if len(ids) > 0 { + err = db.Model(&user.Subscribe{}).Where("id IN ?", ids).Update("status", 3).Error + if err != nil { + logger.Error("[Check Subscription Expire] Update subscribe status failed", logger.Field("error", err.Error())) + return err + } + err = l.sendExpiredNotify(ctx, ids) + if err != nil { + logger.Error("[Check Subscription Expire] Send email failed", logger.Field("error", err.Error())) + return nil + } + if len(list) > 0 { + if err = l.svc.UserModel.ClearSubscribeCache(ctx, list...); err != nil { + logger.Errorw("[Check Subscription Traffic] Clear subscribe cache failed", logger.Field("error", err.Error())) + return err + } + } + logger.Info("[Check Subscription Expire] Update subscribe status", logger.Field("user_ids", ids), logger.Field("count", int64(len(ids)))) + } else { + logger.Info("[Check Subscription Expire] No subscribe need to update") + } + return l.svc.UserModel.ClearSubscribeCache(ctx, list...) + }) + if err != nil { + logger.Info("[CheckSubscription] Transaction failed", logger.Field("error", err.Error())) + } + return nil +} + +func (l *CheckSubscriptionLogic) sendExpiredNotify(ctx context.Context, subs []int64) error { + for _, id := range subs { + sub, err := l.svc.UserModel.FindOneUserSubscribe(ctx, id) + if err != nil { + logger.Errorw("[CheckSubscription] FindOneUserSubscribe failed", logger.Field("error", err.Error())) + continue + } + method, err := l.svc.UserModel.FindUserAuthMethodByUserId(ctx, "email", sub.UserId) + if err != nil { + logger.Errorw("[CheckSubscription] FindUserAuthMethodByUserId failed", logger.Field("error", err.Error()), logger.Field("user_id", sub.UserId)) + continue + } + var taskPayload queue.SendEmailPayload + taskPayload.Email = method.AuthIdentifier + taskPayload.Subject = "Subscription Expired" + tpl, err := template.New("Expired").Parse(l.svc.Config.Email.ExpirationEmailTemplate) + if err != nil { + logger.Errorw("[CheckSubscription] Parse template failed", logger.Field("error", err.Error())) + continue + } + var result bytes.Buffer + err = tpl.Execute(&result, map[string]interface{}{ + "SiteLogo": l.svc.Config.Site.SiteLogo, + "SiteName": l.svc.Config.Site.SiteName, + "ExpireDate": sub.ExpireTime.Format("2006-01-02 15:04:05"), + }) + if err != nil { + logger.Errorw("[CheckSubscription] Execute template failed", logger.Field("error", err.Error())) + continue + } + taskPayload.Content = result.String() + payloadBuy, err := json.Marshal(taskPayload) + if err != nil { + logger.Errorw("[CheckSubscription] Marshal payload failed", logger.Field("error", err.Error())) + continue + } + task := asynq.NewTask(queue.ForthwithSendEmail, payloadBuy, asynq.MaxRetry(3)) + taskInfo, err := l.svc.Queue.Enqueue(task) + if err != nil { + logger.Errorw("[CheckSubscription] Enqueue task failed", logger.Field("error", err.Error()), logger.Field("payload", string(payloadBuy))) + continue + } + logger.Infow("[CheckSubscription] Send email success", + logger.Field("taskID", taskInfo.ID), logger.Field("User", sub.UserId), + logger.Field("Email", method.AuthIdentifier), + ) + } + return nil +} + +func (l *CheckSubscriptionLogic) sendTrafficNotify(ctx context.Context, subs []int64) error { + for _, id := range subs { + sub, err := l.svc.UserModel.FindOneUserSubscribe(ctx, id) + if err != nil { + logger.Errorw("[CheckSubscription] FindOneUserSubscribe failed", logger.Field("error", err.Error())) + continue + } + method, err := l.svc.UserModel.FindUserAuthMethodByUserId(ctx, "email", sub.UserId) + if err != nil { + logger.Errorw("[CheckSubscription] FindUserAuthMethodByUserId failed", logger.Field("error", err.Error()), logger.Field("user_id", sub.UserId)) + continue + } + var taskPayload queue.SendEmailPayload + taskPayload.Email = method.AuthIdentifier + taskPayload.Subject = "Subscription Traffic Exceed" + tpl, err := template.New("Traffic").Parse(l.svc.Config.Email.TrafficExceedEmailTemplate) + if err != nil { + logger.Errorw("[CheckSubscription] Parse template failed", logger.Field("error", err.Error())) + continue + } + var result bytes.Buffer + err = tpl.Execute(&result, map[string]interface{}{ + "SiteLogo": l.svc.Config.Site.SiteLogo, + "SiteName": l.svc.Config.Site.SiteName, + }) + if err != nil { + logger.Errorw("[CheckSubscription] Execute template failed", logger.Field("error", err.Error())) + continue + } + taskPayload.Content = result.String() + payloadBuy, err := json.Marshal(taskPayload) + if err != nil { + logger.Errorw("[CheckSubscription] Marshal payload failed", logger.Field("error", err.Error())) + continue + } + task := asynq.NewTask(queue.ForthwithSendEmail, payloadBuy, asynq.MaxRetry(3)) + taskInfo, err := l.svc.Queue.Enqueue(task) + if err != nil { + logger.Errorw("[CheckSubscription] Enqueue task failed", logger.Field("error", err.Error()), logger.Field("payload", string(payloadBuy))) + continue + } + logger.Infow("[CheckSubscription] Send email success", + logger.Field("taskID", taskInfo.ID), logger.Field("User", sub.UserId), + logger.Field("Email", method.AuthIdentifier), + ) + } + return nil +} diff --git a/queue/logic/traffic/serverDataLogic.go b/queue/logic/traffic/serverDataLogic.go new file mode 100644 index 0000000..83516de --- /dev/null +++ b/queue/logic/traffic/serverDataLogic.go @@ -0,0 +1,166 @@ +package traffic + +import ( + "context" + "encoding/json" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/internal/types" +) + +type ServerDataLogic struct { + svc *svc.ServiceContext +} + +func NewServerDataLogic(svc *svc.ServiceContext) *ServerDataLogic { + return &ServerDataLogic{ + svc: svc, + } +} + +func (l *ServerDataLogic) ProcessTask(ctx context.Context, _ *asynq.Task) error { + serverData := types.ServerTotalDataResponse{} + + top10ServerToday, top10ServerYesterday, top10UserToday, top10UserYesterday := l.getRanking(ctx) + if len(top10ServerToday) == 0 { + top10ServerToday = make([]types.ServerTrafficData, 0) + } + if len(top10ServerYesterday) == 0 { + top10ServerYesterday = make([]types.ServerTrafficData, 0) + } + if len(top10UserToday) == 0 { + top10UserToday = make([]types.UserTrafficData, 0) + } + if len(top10UserYesterday) == 0 { + top10UserYesterday = make([]types.UserTrafficData, 0) + } + serverData.ServerTrafficRankingToday = top10ServerToday + serverData.ServerTrafficRankingYesterday = top10ServerYesterday + serverData.UserTrafficRankingToday = top10UserToday + serverData.UserTrafficRankingYesterday = top10UserYesterday + totalUploadToday, totalDownloadToday, totalDownloadMonthly, totalUploadMonthly := l.trafficCount(ctx) + serverData.TodayUpload = totalUploadToday + serverData.TodayDownload = totalDownloadToday + serverData.MonthlyUpload = totalUploadMonthly + serverData.MonthlyDownload = totalDownloadMonthly + serverData.UpdatedAt = time.Now().UnixMilli() + data, err := json.Marshal(serverData) + if err != nil { + logger.Error("[ServerDataLogic] Marshal server data failed", logger.Field("error", err.Error()), logger.Field("data", serverData)) + return err + } + if err := l.svc.Redis.Set(ctx, config.ServerCountCacheKey, data, -1).Err(); err != nil { + logger.Error("[ServerDataLogic] Set server data failed", logger.Field("error", err.Error())) + return err + } + logger.Info("[ServerDataLogic] Update server data success") + return nil +} + +func (l *ServerDataLogic) getRanking(ctx context.Context) (top10ServerToday, top10ServerYesterday []types.ServerTrafficData, top10UserToday, top10UserYesterday []types.UserTrafficData) { + now := time.Now() + // 获取服务器流量排行榜 + serverToday, err := l.svc.TrafficLogModel.TopServersTrafficByDay(ctx, now, 10) + if err != nil { + logger.Error("[ServerDataLogic] Get top servers traffic by day failed", logger.Field("error", err.Error())) + } else { + for _, s := range serverToday { + if s.ServerId == 0 { + continue + } + serverInfo, err := l.svc.ServerModel.FindOne(ctx, s.ServerId) + if err != nil { + logger.Error("[ServerDataLogic] Find server failed", logger.Field("error", err.Error())) + continue + } + top10ServerToday = append(top10ServerToday, types.ServerTrafficData{ + ServerId: s.ServerId, + Name: serverInfo.Name, + Upload: s.Upload, + Download: s.Download, + }) + } + } + + serverYesterday, err := l.svc.TrafficLogModel.TopServersTrafficByDay(ctx, now.AddDate(0, 0, -1), 10) + if err != nil { + logger.Error("[ServerDataLogic] Get top servers traffic by day failed", logger.Field("error", err.Error())) + } else { + for _, s := range serverYesterday { + serverInfo, err := l.svc.ServerModel.FindOne(ctx, s.ServerId) + if err != nil { + logger.Error("[ServerDataLogic] Find server failed", logger.Field("error", err.Error())) + continue + } + top10ServerYesterday = append(top10ServerYesterday, types.ServerTrafficData{ + ServerId: s.ServerId, + Name: serverInfo.Name, + Upload: s.Upload, + Download: s.Download, + }) + } + } + + // 获取用户流量排行榜 + userToday, err := l.svc.TrafficLogModel.TopUsersTrafficByDay(ctx, now, 10) + if err != nil { + logger.Error("[ServerDataLogic] Get top users traffic by day failed", logger.Field("error", err.Error())) + } else { + for _, u := range userToday { + //userInfo, err := l.svc.UserModel.FindOne(ctx, u.UserId) + //if err != nil { + // logx.Error("[ServerDataLogic] Find user failed", logx.Field("error", err.Error())) + // continue + //} + top10UserToday = append(top10UserToday, types.UserTrafficData{ + SID: u.UserId, + Upload: u.Upload, + Download: u.Download, + }) + } + } + + userYesterday, err := l.svc.TrafficLogModel.TopUsersTrafficByDay(ctx, now.AddDate(0, 0, -1), 10) + if err != nil { + logger.Error("[ServerDataLogic] Get top users traffic by day failed", logger.Field("error", err.Error())) + } else { + for _, u := range userYesterday { + //userInfo, err := l.svc.UserModel.FindOne(ctx, u.UserId) + //if err != nil { + // logx.Error("[ServerDataLogic] Find user failed", logx.Field("error", err.Error())) + // continue + //} + top10UserYesterday = append(top10UserYesterday, types.UserTrafficData{ + SID: u.UserId, + Upload: u.Upload, + Download: u.Download, + }) + } + } + return +} + +func (l *ServerDataLogic) trafficCount(ctx context.Context) (totalUploadToday, totalDownloadToday, totalDownloadMonthly, totalUploadMonthly int64) { + now := time.Now() + today, err := l.svc.TrafficLogModel.QueryTrafficByDay(ctx, now) + if err != nil { + logger.Error("[ServerDataLogic] Query traffic by day failed", logger.Field("error", err.Error())) + } else { + totalUploadToday = today.Upload + totalDownloadToday = today.Download + } + + monthly, err := l.svc.TrafficLogModel.QueryTrafficByMonthly(ctx, now) + if err != nil { + logger.Error("[ServerDataLogic] Query traffic by monthly failed", logger.Field("error", err.Error())) + } else { + totalUploadMonthly = monthly.Upload + totalDownloadMonthly = monthly.Download + } + return +} diff --git a/queue/logic/traffic/trafficStatisticsLogic.go b/queue/logic/traffic/trafficStatisticsLogic.go new file mode 100644 index 0000000..d9b3a6d --- /dev/null +++ b/queue/logic/traffic/trafficStatisticsLogic.go @@ -0,0 +1,98 @@ +package traffic + +import ( + "context" + "encoding/json" + "time" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/model/traffic" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/queue/types" +) + +//goland:noinspection GoNameStartsWithPackageName +type TrafficStatisticsLogic struct { + svc *svc.ServiceContext +} + +func NewTrafficStatisticsLogic(svc *svc.ServiceContext) *TrafficStatisticsLogic { + return &TrafficStatisticsLogic{ + svc: svc, + } +} + +func (l *TrafficStatisticsLogic) ProcessTask(ctx context.Context, task *asynq.Task) error { + var payload types.TrafficStatistics + if err := json.Unmarshal(task.Payload(), &payload); err != nil { + logger.WithContext(ctx).Error("[TrafficStatistics] Unmarshal payload failed", + logger.Field("error", err.Error()), + logger.Field("payload", string(task.Payload())), + ) + return nil + } + if len(payload.Logs) == 0 { + logger.WithContext(ctx).Error("[TrafficStatistics] Payload is empty") + return nil + } + // query server info + serverInfo, err := l.svc.ServerModel.FindOne(ctx, payload.ServerId) + if err != nil { + logger.WithContext(ctx).Error("[TrafficStatistics] Find server info failed", + logger.Field("serverId", payload.ServerId), + logger.Field("error", err.Error()), + ) + return nil + } + if serverInfo.TrafficRatio == 0 { + logger.WithContext(ctx).Error("[TrafficStatistics] Server log ratio is 0", + logger.Field("serverId", payload.ServerId), + ) + return nil + } + now := time.Now() + realTimeMultiplier := l.svc.NodeMultiplierManager.GetMultiplier(now) + for _, log := range payload.Logs { + // update user subscribe with log + d := int64(float32(log.Download) * serverInfo.TrafficRatio * realTimeMultiplier) + u := int64(float32(log.Upload) * serverInfo.TrafficRatio * realTimeMultiplier) + if err := l.svc.UserModel.UpdateUserSubscribeWithTraffic(ctx, log.SID, d, u); err != nil { + logger.WithContext(ctx).Error("[TrafficStatistics] Update user subscribe with log failed", + logger.Field("sid", log.SID), + logger.Field("download", float32(log.Download)*serverInfo.TrafficRatio), + logger.Field("upload", float32(log.Upload)*serverInfo.TrafficRatio), + logger.Field("error", err.Error()), + ) + continue + } + // query user Subscribe Info + sub, err := l.svc.UserModel.FindOneSubscribe(ctx, log.SID) + if err != nil { + logger.WithContext(ctx).Error("[TrafficStatistics] Find user Subscribe Info failed", + logger.Field("uid", log.SID), + logger.Field("error", err.Error()), + ) + continue + } + + // create log log + if err := l.svc.TrafficLogModel.Insert(ctx, &traffic.TrafficLog{ + ServerId: payload.ServerId, + SubscribeId: log.SID, + UserId: sub.UserId, + Upload: u, + Download: d, + Timestamp: now, + }); err != nil { + logger.WithContext(ctx).Error("[TrafficStatistics] Create log log failed", + logger.Field("uid", log.SID), + logger.Field("download", float32(log.Download)*serverInfo.TrafficRatio), + logger.Field("upload", float32(log.Upload)*serverInfo.TrafficRatio), + logger.Field("error", err.Error()), + ) + } + } + return nil +} diff --git a/queue/queue.go b/queue/queue.go new file mode 100644 index 0000000..6384412 --- /dev/null +++ b/queue/queue.go @@ -0,0 +1,51 @@ +package queue + +import ( + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/ppanel-server/queue/handler" +) + +type Service struct { + svc *svc.ServiceContext + server *asynq.Server +} + +func NewService(svc *svc.ServiceContext) *Service { + return &Service{ + svc: svc, + server: initService(svc), + } +} + +func (m *Service) Start() { + logger.Infof("start consumer service") + mux := asynq.NewServeMux() + // register tasks + handler.RegisterHandlers(mux, m.svc) + if err := m.server.Run(mux); err != nil { + logger.Error("consumer service error", logger.LogField{ + Key: "error", + Value: err.Error(), + }) + } +} + +func (m *Service) Stop() { + logger.Info("stop consumer service") + m.server.Stop() +} + +func initService(svc *svc.ServiceContext) *asynq.Server { + return asynq.NewServer( + asynq.RedisClientOpt{Addr: svc.Config.Redis.Host, Password: svc.Config.Redis.Pass, DB: 5}, + asynq.Config{ + IsFailure: func(err error) bool { + logger.Error("consumer service error", logger.Field("error", err.Error())) + return true + }, + Concurrency: 20, + }, + ) +} diff --git a/queue/types/country.go b/queue/types/country.go new file mode 100644 index 0000000..13b9e0c --- /dev/null +++ b/queue/types/country.go @@ -0,0 +1,11 @@ +package types + +const ( + // ForthwithGetCountry forthwith country get + ForthwithGetCountry = "forthwith:country:get" +) + +type GetNodeCountry struct { + Protocol string `json:"protocol"` + ServerAddr string `json:"server_addr"` +} diff --git a/queue/types/email.go b/queue/types/email.go new file mode 100644 index 0000000..2cbdf2e --- /dev/null +++ b/queue/types/email.go @@ -0,0 +1,14 @@ +package types + +const ( + // ForthwithSendEmail forthwith send email + ForthwithSendEmail = "forthwith:email:send" +) + +type ( + SendEmailPayload struct { + Email string `json:"to"` + Subject string `json:"subject"` + Content string `json:"content"` + } +) diff --git a/queue/types/order.go b/queue/types/order.go new file mode 100644 index 0000000..5d05f9f --- /dev/null +++ b/queue/types/order.go @@ -0,0 +1,18 @@ +package types + +const ( + DeferCloseOrder = "defer:order:close" + ForthwithActivateOrder = "forthwith:order:activate" +) + +type ( + DeferCheckOrderLogic struct { + OrderNo string `json:"order_no"` + } + DeferCloseOrderPayload struct { + OrderNo string `json:"order_no"` + } + ForthwithActivateOrderPayload struct { + OrderNo string `json:"order_no"` + } +) diff --git a/queue/types/scheduler.go b/queue/types/scheduler.go new file mode 100644 index 0000000..13b3028 --- /dev/null +++ b/queue/types/scheduler.go @@ -0,0 +1,7 @@ +package types + +const ( + SchedulerCheckSubscription = "scheduler:check:subscription" + SchedulerTotalServerData = "scheduler:total:server" + SchedulerCheckOrder = "scheduler:check:order" +) diff --git a/queue/types/server.go b/queue/types/server.go new file mode 100644 index 0000000..75ec89c --- /dev/null +++ b/queue/types/server.go @@ -0,0 +1,39 @@ +package types + +const ForthwithTrafficStatistics = "forthwith:traffic:statistics" + +type UserTraffic struct { + SID int64 `json:"uid"` + Upload int64 `json:"upload"` + Download int64 `json:"download"` +} + +type TrafficStatistics struct { + ServerId int64 `json:"server_id"` + Logs []UserTraffic `json:"logs"` +} + +type NodeStatus struct { + OnlineUsers []OnlineUser `json:"online_users"` + Status ServerStatus `json:"status"` + LastAt int64 `json:"last_at"` +} + +type OnlineUser struct { + UID int64 `json:"uid"` + IP string `json:"ip"` +} + +type ServerStatus struct { + Cpu float64 `json:"cpu"` + Mem float64 `json:"mem"` + Disk float64 `json:"disk"` + UpdatedAt int64 `json:"updated_at"` +} + +type ServerTrafficCount struct { + ServerId int64 `json:"server_id"` + Name string `json:"name"` + Today int64 `json:"today"` + Yesterday int64 `json:"yesterday"` +} diff --git a/queue/types/sms.go b/queue/types/sms.go new file mode 100644 index 0000000..8d9f9e7 --- /dev/null +++ b/queue/types/sms.go @@ -0,0 +1,15 @@ +package types + +const ( + // ForthwithSendEmail forthwith send email + ForthwithSendSms = "forthwith:sms:send" +) + +type ( + SendSmsPayload struct { + Type uint8 `json:"type"` + Telephone string `json:"telephone"` + TelephoneArea string `json:"area"` + Content string `json:"content"` + } +) diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..e60f85a --- /dev/null +++ b/readme.md @@ -0,0 +1,71 @@ +## Directory Structure + +```text +. +├── etc +├── cmd +├── queue +├── generate +├── initialize +├── go.mod +├── internal +│ ├── config +│ ├── handler +│ ├── middleware +│ ├── logic +│ ├── svc +│ ├── types +│ └── model +├── scheduler +├── pkg +└── script +``` +- apis: API definition files +- etc: Directory for static configuration files +- cmd:Application entry point +- queue:Queue consumption service +- generate:Code generation tools +- initialize: Initialization system configuration +- internal:Internal modules + - config:Configuration file parsing + - handler:HTTP interface handling, with `handler` as the fixed suffix + - middleware:HTTP middleware + - logic:Business logic handling, with `logic` as the fixed suffix + - svc:Service layer encapsulation + - types:Type definitions + - model:Data models +- scheduler:Scheduled tasks +- pkg: Common utility code +- script:Build scripts + + +##### Generate Code + +```bash +$ chmod +x script/generate.sh +$ ./script/generate.sh +``` + +##### Generate Swagger + +```bash +$ goctl api plugin -plugin goctl-swagger='swagger -filename ppanel.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ppanel.api -dir . +``` + +##### Format API File + +```bash +$ goctl api format --dir api/user.api +``` + +##### Build + +```bash +$ go build -o ppanel ppanel.go +``` + +##### Run + +```bash +$ ./ppanel run --config etc/ppanel.yaml +``` \ No newline at end of file diff --git a/scheduler/scheduler.go b/scheduler/scheduler.go new file mode 100644 index 0000000..fa6b6f8 --- /dev/null +++ b/scheduler/scheduler.go @@ -0,0 +1,62 @@ +package scheduler + +import ( + "time" + + "github.com/perfect-panel/ppanel-server/pkg/logger" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/ppanel-server/queue/types" +) + +type Service struct { + svc *svc.ServiceContext + server *asynq.Scheduler +} + +func NewService(svc *svc.ServiceContext) *Service { + return &Service{ + svc: svc, + server: initService(svc), + } +} + +func (m *Service) Start() { + logger.Infof("start scheduler service") + // schedule check subscription task: every 60 seconds + checkTask := asynq.NewTask(types.SchedulerCheckSubscription, nil) + if _, err := m.server.Register("@every 60s", checkTask); err != nil { + logger.Errorf("register check subscription task failed: %s", err.Error()) + } + // schedule total server data task: every 5 minutes + totalServerDataTask := asynq.NewTask(types.SchedulerTotalServerData, nil) + if _, err := m.server.Register("@every 180s", totalServerDataTask); err != nil { + logger.Errorf("register total server data task failed: %s", err.Error()) + } + + // schedule total server data task: every 5 minutes + checkOrderTask := asynq.NewTask(types.SchedulerCheckOrder, nil) + if _, err := m.server.Register("@every 10s", checkOrderTask); err != nil { + logger.Errorf("register check order task failed: %s", err.Error()) + } + + if err := m.server.Run(); err != nil { + logger.Errorf("run scheduler failed: %s", err.Error()) + } +} + +func (m *Service) Stop() { + logger.Info("stop scheduler service") + m.server.Shutdown() +} + +func initService(svc *svc.ServiceContext) *asynq.Scheduler { + location, _ := time.LoadLocation("Asia/Shanghai") + return asynq.NewScheduler( + asynq.RedisClientOpt{Addr: svc.Config.Redis.Host, Password: svc.Config.Redis.Pass, DB: 5}, + &asynq.SchedulerOpts{ + Location: location, + }, + ) +} diff --git a/script/generate.sh b/script/generate.sh new file mode 100644 index 0000000..ea116d5 --- /dev/null +++ b/script/generate.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +OS_TYPE=$(uname) +ARCH_TYPE=$(uname -m) + +if [[ "$OS_TYPE" == "Linux" ]]; then + echo "The current operating system is Linux" + if [[ "$ARCH_TYPE" == "x86_64" ]]; then + echo "Architecture: amd64" + ./generate/gopure-linux-amd64 api go -api *.api -dir . -style goZero + elif [[ "$ARCH_TYPE" == "aarch64" ]]; then + echo "Architecture: arm64" + ./generate/gopure-linux-arm64 api go -api *.api -dir . -style goZero + else + echo "Unrecognized architecture: $ARCH_TYPE" + fi +elif [[ "$OS_TYPE" == "Darwin" ]]; then + echo "The current operating system is macOS" + if [[ "$ARCH_TYPE" == "x86_64" ]]; then + echo "Architecture: amd64" + ./generate/gopure-darwin-amd64 api go -api *.api -dir . -style goZero + elif [[ "$ARCH_TYPE" == "arm64" ]]; then + echo "Architecture: arm64" + ./generate/gopure-darwin-arm64 api go -api *.api -dir . -style goZero + else + echo "Unrecognized architecture: $ARCH_TYPE" + fi +elif [[ "$OS_TYPE" == "CYGWIN"* || "$OS_TYPE" == "MINGW"* ]]; then + echo "The current operating system is Windows" + if [[ "$ARCH_TYPE" == "x86_64" ]]; then + echo "Architecture: amd64" + ./generate/gopure-amd64.exe api go -api *.api -dir . -style goZero + elif [[ "$ARCH_TYPE" == "arm64" ]]; then + echo "Architecture: arm64" + ./generate/gopure-arm64.exe api go -api *.api -dir . -style goZero + else + echo "Unrecognized architecture: $ARCH_TYPE" + fi +else + echo "Unrecognized operating system: $OS_TYPE" +fi diff --git a/script/install.sh b/script/install.sh new file mode 100644 index 0000000..27f102c --- /dev/null +++ b/script/install.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +# 检查是否以 root 用户运行 +if [ "$(id -u)" -ne 0 ]; then + echo "请以 root 用户运行此脚本" + exit 1 +fi + +# 系统检测,确定使用的包管理工具 +if [ -f /etc/debian_version ]; then + # Ubuntu / Debian 系统 + PKG_MANAGER="apt-get" +elif [ -f /etc/redhat-release ]; then + # CentOS 系统 + PKG_MANAGER="yum" +else + echo "不支持的系统" + exit 1 +fi + +# 检查 jq 是否已安装,若未安装则自动安装 +if ! command -v jq &> /dev/null; then + echo "jq 未安装,正在安装 jq ..." + if [ "$PKG_MANAGER" == "apt-get" ]; then + apt-get update && apt-get install -y jq + elif [ "$PKG_MANAGER" == "yum" ]; then + yum install -y jq + else + echo "无法安装 jq,未知的包管理器" + exit 1 + fi +fi + +# 获取最新的版本号 +VERSION=$(curl -s https://api.github.com/repos/perfect-panel/ppanel/releases/latest | jq -r .tag_name) + +if [ "$VERSION" == "null" ]; then + echo "无法获取最新版本号,请检查网络或 GitHub API 状态" + exit 1 +fi + +# 安装路径 +INSTALL_DIR="/opt/ppanel-server" +SERVICE_NAME="ppanel" + +# 下载并解压二进制文件 +echo "开始下载 ppanel 二进制文件,版本:$VERSION ..." +wget https://github.com/perfect-panel/ppanel/releases/download/$VERSION/ppanel-server-linux-amd64.tar.gz -O /tmp/ppanel-server-linux-amd64.tar.gz + +# 创建安装目录 +if [ ! -d "$INSTALL_DIR" ]; then + mkdir -p "$INSTALL_DIR" +fi + +# 解压文件到安装目录 +echo "解压文件到 $INSTALL_DIR ..." +tar -zxvf /tmp/ppanel-server-linux-amd64.tar.gz -C "$INSTALL_DIR" --strip-components=1 + +# 给二进制文件赋予执行权限 +chmod +x "$INSTALL_DIR/ppanel-server" + +# 创建 systemd 服务文件 +echo "创建 systemd 服务文件 ..." +cat > /etc/systemd/system/$SERVICE_NAME.service <